From 145c111e35452f1ba0010f4a6cdd9a677209f653 Mon Sep 17 00:00:00 2001 From: joocer Date: Thu, 25 Apr 2024 21:21:14 +0100 Subject: [PATCH 1/3] #1607 --- opteryx/compiled/functions/__init__.py | 3 - opteryx/compiled/functions/vectors.pyx_sparse | 322 ++++++++++++++++++ opteryx/compiled/list_ops/cython_list_ops.pyx | 44 +-- opteryx/compiled/structures/__init__.py | 4 + .../{functions => structures}/hash_table.pyx | 0 .../{functions => structures}/node.pyx | 0 opteryx/models/__init__.py | 2 +- opteryx/operators/distinct_node.py | 4 +- opteryx/operators/inner_join_node.py | 2 +- opteryx/operators/left_join_node.py | 2 +- .../logical_planner_builders.py | 9 +- setup.py | 8 +- .../test_shapes_and_errors_battery.py | 4 + 13 files changed, 356 insertions(+), 48 deletions(-) create mode 100644 opteryx/compiled/functions/vectors.pyx_sparse create mode 100644 opteryx/compiled/structures/__init__.py rename opteryx/compiled/{functions => structures}/hash_table.pyx (100%) rename opteryx/compiled/{functions => structures}/node.pyx (100%) diff --git a/opteryx/compiled/functions/__init__.py b/opteryx/compiled/functions/__init__.py index 8eeeb51f4..e7680a12d 100644 --- a/opteryx/compiled/functions/__init__.py +++ b/opteryx/compiled/functions/__init__.py @@ -1,7 +1,4 @@ from .cython_functions import numpy_array_get_element -from .hash_table import HashSet -from .hash_table import HashTable -from .hash_table import distinct from .ip_address import ip_in_cidr from .vectors import possible_match from .vectors import possible_match_indices diff --git a/opteryx/compiled/functions/vectors.pyx_sparse b/opteryx/compiled/functions/vectors.pyx_sparse new file mode 100644 index 000000000..49ab31bc7 --- /dev/null +++ b/opteryx/compiled/functions/vectors.pyx_sparse @@ -0,0 +1,322 @@ +# cython: language_level=3 +# cython: boundscheck=False +# cython: wraparound=False +# cython: nonecheck=False +# cython: overflowcheck=False + +import numpy as np +cimport numpy as cnp +cimport cython + +from libc.stdint cimport uint32_t, uint16_t, uint64_t +from cpython cimport PyUnicode_AsUTF8String, PyBytes_GET_SIZE +from cpython.bytes cimport PyBytes_AsString +from libcpp.unordered_map cimport unordered_map +from libcpp.pair cimport pair + +cdef double GOLDEN_RATIO_APPROX = 1.618033988749895 +cdef uint32_t VECTOR_SIZE = 8192 + +cdef dict irregular_lemmas = { + b'are': b'is', + b'arose': b'arise', + b'awoke': b'awake', + b'was': b'be', + b'were': b'be', + b'born': b'bear', + b'bore': b'bear', + b'be': b'is', + b'became': b'become', + b'began': b'begin', + b'bent': b'bend', + b'best': b'good', + b'better': b'good', + b'bit': b'bite', + b'bled': b'bleed', + b'blew': b'blow', + b'broke': b'break', + b'bred': b'breed', + b'brought': b'bring', + b'built': b'build', + b'burnt': b'burn', + b'burst': b'burst', + b'bought': b'buy', + b'caught': b'catch', + b'chose': b'choose', + b'clung': b'cling', + b'came': b'come', + b'crept': b'creep', + b'dealt': b'deal', + b'dug': b'dig', + b'did': b'do', + b'done': b'do', + b'drew': b'draw', + b'drank': b'drink', + b'drove': b'drive', + b'ate': b'eat', + b'famous': b'famous', + b'fell': b'fall', + b'fed': b'feed', + b'felt': b'feel', + b'fought': b'fight', + b'found': b'find', + b'fled': b'flee', + b'flung': b'fling', + b'flew': b'fly', + b'forbade': b'forbid', + b'forgot': b'forget', + b'forgave': b'forgive', + b'froze': b'freeze', + b'got': b'get', + b'gave': b'give', + b'went': b'go', + b'grew': b'grow', + b'had': b'have', + b'heard': b'hear', + b'hid': b'hide', + b'his': b'his', + b'held': b'hold', + b'kept': b'keep', + b'knew': b'know', + b'knelt': b'kneel', + b'knew': b'know', + b'led': b'lead', + b'leapt': b'leap', + b'learnt': b'learn', + b'left': b'leave', + b'lent': b'lend', + b'lay': b'lie', + b'lit': b'light', + b'lost': b'lose', + b'made': b'make', + b'meant': b'mean', + b'met': b'meet', + b'men': b'man', + b'paid': b'pay', + b'people': b'person', + b'rode': b'ride', + b'rang': b'ring', + b'rose': b'rise', + b'ran': b'run', + b'said': b'say', + b'saw': b'see', + b'sold': b'sell', + b'sent': b'send', + b'shone': b'shine', + b'shot': b'shoot', + b'showed': b'show', + b'sang': b'sing', + b'sank': b'sink', + b'sat': b'sit', + b'slept': b'sleep', + b'spoke': b'speak', + b'spent': b'spend', + b'spun': b'spin', + b'stood': b'stand', + b'stole': b'steal', + b'stuck': b'stick', + b'strove': b'strive', + b'sung': b'sing', + b'swore': b'swear', + b'swept': b'sweep', + b'swam': b'swim', + b'swung': b'swing', + b'took': b'take', + b'taught': b'teach', + b'tore': b'tear', + b'told': b'tell', + b'thought': b'think', + b'threw': b'throw', + b'trod': b'tread', + b'understood': b'understand', + b'went': b'go', + b'woke': b'wake', + b'wore': b'wear', + b'won': b'win', + b'wove': b'weave', + b'wept': b'weep', + b'would': b'will', + b'wrote': b'write' +} + +cdef inline uint32_t djb2_hash(char* byte_array, uint64_t length) nogil: + """ + Hashes a byte array using the djb2 algorithm, designed to be called without + holding the Global Interpreter Lock (GIL). + + Parameters: + byte_array: char* + The byte array to hash. + length: uint64_t + The length of the byte array. + + Returns: + The hash value. + """ + cdef uint32_t hash_value = 5381 + cdef uint32_t i = 0 + for i in range(length): + hash_value = ((hash_value << 5) + hash_value) + byte_array[i] + return hash_value + + +from libcpp.vector cimport vector + +def vectorize(list[bytes] tokens): + cdef uint16_t hash_1, hash_2 + cdef bytes token_bytes + cdef uint32_t token_size + cdef uint32_t hash + + cdef vector[uint32_t] vector_map + vector_map.resize(VECTOR_SIZE, 0) # Initialize vector with zero + + for token_bytes in tokens: + token_size = PyBytes_GET_SIZE(token_bytes) + if token_size > 1: + hash = djb2_hash(token_bytes, token_size) + hash_1 = hash & (VECTOR_SIZE - 1) + hash_2 = (hash >> 8) & (VECTOR_SIZE - 1) + vector_map[hash_1] += 1 + vector_map[hash_2] += 1 + + return [(i, vector_map[i]) for i in range(VECTOR_SIZE) if vector_map[i] > 0] + + + + +def possible_match(list query_tokens, cnp.ndarray[cnp.uint16_t, ndim=1] vector): + cdef uint16_t hash_1 + cdef uint16_t hash_2 + cdef bytes token_bytes + cdef uint32_t token_size + + for token_bytes in query_tokens: + token_size = PyBytes_GET_SIZE(token_bytes) + if token_size > 1: + hash_1 = djb2_hash(token_bytes, token_size) + hash_2 = ((hash_1 * GOLDEN_RATIO_APPROX)) & (VECTOR_SIZE - 1) + if vector[hash_1 & (VECTOR_SIZE - 1)] == 0 or vector[hash_2] == 0: + return False # If either position is zero, the token cannot be present + return True + + +def possible_match_indices(cnp.ndarray[cnp.uint16_t, ndim=1] indices, cnp.ndarray[cnp.uint16_t, ndim=1] vector): + """ + Check if all specified indices in 'indices' have non-zero values in 'vector'. + + Parameters: + indices: cnp.ndarray[cnp.uint16_t, ndim=1] + Array of indices to check in the vector. + vector: cnp.ndarray[cnp.uint16_t, ndim=1] + Array where non-zero values are expected at the indices specified by 'indices'. + + Returns: + bool: True if all specified indices have non-zero values, otherwise False. + """ + cdef int i + for i in range(indices.shape[0]): + if vector[indices[i]] == 0: + return False + return True + + +from libc.string cimport strlen, strcpy, strtok, strchr +from libc.stdlib cimport malloc, free + +cdef char* strdup(const char* s) nogil: + cdef char* d = malloc(strlen(s) + 1) + if d: + strcpy(d, s) + return d + +cpdef list tokenize_and_remove_punctuation(str text, set stop_words): + cdef: + char* token + char* word + char* c_text + bytes py_text = PyUnicode_AsUTF8String(text) + list tokens = [] + int i + int j + + # Duplicate the C string because strtok modifies the input string + c_text = strdup(PyBytes_AsString(py_text)) + + try: + token = strtok(c_text, " ") + while token != NULL: + word = malloc(strlen(token) + 1) + i = 0 + j = 0 + while token[i] != 0: + if 97 <= token[i] <= 122 or 48 <= token[i] <= 57: + word[j] = token[i] + j += 1 + elif 65 <= token[i] <= 90: + # Convert to lowercase if it's uppercase + word[j] = token[i] + 32 + j += 1 + elif token[i] == 45 and j > 0: + word[j] = token[i] + j += 1 + i += 1 + word[j] = 0 + if j > 1: + if word in irregular_lemmas: + lemma = strdup(irregular_lemmas[word]) + else: + # Perform lemmatization + lemma = lemmatize(word, j) + + # Append the lemma if it's not a stop word + if lemma not in stop_words: + tokens.append(lemma) + + free(word) + token = strtok(NULL, " ") + finally: + free(c_text) + + return tokens + + +from libc.string cimport strlen, strncmp, strcpy, strcat + + +from libc.string cimport strlen, strncmp + +cpdef inline bytes lemmatize(char* word, int word_len): + + # Check 'ing' suffix + if word_len > 5 and strncmp(word + word_len - 3, b"ing", 3) == 0: + if word[word_len - 4] == word[word_len - 5]: # Double consonant + return word[:word_len - 4] + return word[:word_len - 3] + + # Check 'ed' suffix + if word_len > 4 and strncmp(word + word_len - 2, b"ed", 2) == 0: + if word[word_len - 3] == word[word_len - 4]: + return word[:word_len - 3] + return word[:word_len - 2] + + # Check 'ly' suffix + if word_len > 5 and strncmp(word + word_len - 2, b"ly", 2) == 0: + if word[word_len - 3] == word[word_len - 4]: + return word[:word_len - 3] + return word[:word_len - 2] + + # Check 'ation' suffix + if word_len > 8 and strncmp(word + word_len - 5, b"ation", 5) == 0: + return word[:word_len - 5] + b'e' + + # Check 'ment' suffix + if word_len > 8 and strncmp(word + word_len - 4, b"ation", 4) == 0: + return word[:word_len - 4] + + # Check 's' suffix + if word_len > 2 and strncmp(word + word_len - 1, b"s", 1) == 0: + return word[:word_len - 1] + + return word # Return the original if no suffix matches + diff --git a/opteryx/compiled/list_ops/cython_list_ops.pyx b/opteryx/compiled/list_ops/cython_list_ops.pyx index 7caf58daa..8b4336df5 100644 --- a/opteryx/compiled/list_ops/cython_list_ops.pyx +++ b/opteryx/compiled/list_ops/cython_list_ops.pyx @@ -1,25 +1,23 @@ # cython: language_level=3 +# cython: boundscheck=False +# cython: wraparound=False + #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION import cython import numpy - cimport numpy as cnp - from cython import Py_ssize_t from cython.parallel import prange - from numpy cimport int64_t, ndarray cnp.import_array() -@cython.boundscheck(False) -@cython.wraparound(False) cpdef cnp.ndarray[cnp.npy_bool, ndim=1] cython_allop_eq(object literal, cnp.ndarray arr): cdef: int i, j - cnp.ndarray[cnp.npy_bool, ndim=1] result = numpy.empty(arr.shape[0], dtype=bool) + cnp.ndarray[cnp.npy_bool, ndim=1] result = numpy.full(arr.shape[0], False, dtype=bool) for i in range(arr.shape[0]): row = arr[i] @@ -27,7 +25,6 @@ cpdef cnp.ndarray[cnp.npy_bool, ndim=1] cython_allop_eq(object literal, cnp.ndar result[i] = False continue - result[i] = True for j in range(row.shape[0]): if row[j] != literal: result[i] = False @@ -36,17 +33,14 @@ cpdef cnp.ndarray[cnp.npy_bool, ndim=1] cython_allop_eq(object literal, cnp.ndar return result -@cython.boundscheck(False) -@cython.wraparound(False) cpdef cnp.ndarray[cnp.npy_bool, ndim=1] cython_allop_neq(object literal, cnp.ndarray arr): cdef: int i, j - cnp.ndarray[cnp.npy_bool, ndim=1] result = numpy.empty(arr.shape[0], dtype=bool) + cnp.ndarray[cnp.npy_bool, ndim=1] result = numpy.full(arr.shape[0], True, dtype=bool) for i in range(arr.shape[0]): row = arr[i] - result[i] = True for j in range(row.shape[0]): if row[j] == literal: result[i] = False @@ -55,8 +49,6 @@ cpdef cnp.ndarray[cnp.npy_bool, ndim=1] cython_allop_neq(object literal, cnp.nda return result -@cython.boundscheck(False) -@cython.wraparound(False) cpdef cnp.ndarray[cnp.npy_bool, ndim=1] cython_anyop_eq(object literal, cnp.ndarray arr): cdef: Py_ssize_t i, j @@ -72,8 +64,6 @@ cpdef cnp.ndarray[cnp.npy_bool, ndim=1] cython_anyop_eq(object literal, cnp.ndar return result -@cython.boundscheck(False) -@cython.wraparound(False) cpdef cnp.ndarray[cnp.npy_bool, ndim=1] cython_anyop_neq(object literal, cnp.ndarray arr): cdef: int i, j @@ -92,16 +82,13 @@ cpdef cnp.ndarray[cnp.npy_bool, ndim=1] cython_anyop_neq(object literal, cnp.nda return result -@cython.boundscheck(False) -@cython.wraparound(False) cpdef cnp.ndarray[cnp.npy_bool, ndim=1] cython_anyop_gt(object literal, cnp.ndarray arr): cdef: int i, j - cnp.ndarray[cnp.npy_bool, ndim=1] result = numpy.empty(arr.shape[0], dtype=bool) + cnp.ndarray[cnp.npy_bool, ndim=1] result = numpy.full(arr.shape[0], False, dtype=bool) for i in range(arr.shape[0]): row = arr[i] - result[i] = False for j in range(row.shape[0]): if row[j] > literal: result[i] = True @@ -110,16 +97,13 @@ cpdef cnp.ndarray[cnp.npy_bool, ndim=1] cython_anyop_gt(object literal, cnp.ndar return result -@cython.boundscheck(False) -@cython.wraparound(False) cpdef cnp.ndarray[cnp.npy_bool, ndim=1] cython_anyop_lt(object literal, cnp.ndarray arr): cdef: int i, j - cnp.ndarray[cnp.npy_bool, ndim=1] result = numpy.empty(arr.shape[0], dtype=bool) + cnp.ndarray[cnp.npy_bool, ndim=1] result = numpy.full(arr.shape[0], False, dtype=bool) for i in range(arr.shape[0]): row = arr[i] - result[i] = False for j in range(row.shape[0]): if row[j] < literal: result[i] = True @@ -127,16 +111,14 @@ cpdef cnp.ndarray[cnp.npy_bool, ndim=1] cython_anyop_lt(object literal, cnp.ndar return result -@cython.boundscheck(False) -@cython.wraparound(False) + cpdef cnp.ndarray[cnp.npy_bool, ndim=1] cython_anyop_lte(object literal, cnp.ndarray arr): cdef: int i, j - cnp.ndarray[cnp.npy_bool, ndim=1] result = numpy.empty(arr.shape[0], dtype=bool) + cnp.ndarray[cnp.npy_bool, ndim=1] result = numpy.full(arr.shape[0], False, dtype=bool) for i in range(arr.shape[0]): row = arr[i] - result[i] = False for j in range(row.shape[0]): if row[j] <= literal: result[i] = True @@ -144,19 +126,17 @@ cpdef cnp.ndarray[cnp.npy_bool, ndim=1] cython_anyop_lte(object literal, cnp.nda return result -@cython.boundscheck(False) -@cython.wraparound(False) + cpdef cnp.ndarray[cnp.npy_bool, ndim=1] cython_anyop_gte(object literal, cnp.ndarray arr): cdef: int i, j - cnp.ndarray[cnp.npy_bool, ndim=1] result = numpy.empty(arr.shape[0], dtype=bool) + cnp.ndarray[cnp.npy_bool, ndim=1] result = numpy.full(arr.shape[0], False, dtype=bool) for i in range(arr.shape[0]): row = arr[i] - result[i] = False for j in range(row.shape[0]): if row[j] >= literal: result[i] = True break - return result \ No newline at end of file + return result diff --git a/opteryx/compiled/structures/__init__.py b/opteryx/compiled/structures/__init__.py new file mode 100644 index 000000000..73525a004 --- /dev/null +++ b/opteryx/compiled/structures/__init__.py @@ -0,0 +1,4 @@ +from .hash_table import HashSet +from .hash_table import HashTable +from .hash_table import distinct +from .node import Node diff --git a/opteryx/compiled/functions/hash_table.pyx b/opteryx/compiled/structures/hash_table.pyx similarity index 100% rename from opteryx/compiled/functions/hash_table.pyx rename to opteryx/compiled/structures/hash_table.pyx diff --git a/opteryx/compiled/functions/node.pyx b/opteryx/compiled/structures/node.pyx similarity index 100% rename from opteryx/compiled/functions/node.pyx rename to opteryx/compiled/structures/node.pyx diff --git a/opteryx/models/__init__.py b/opteryx/models/__init__.py index d9bb1a6b3..21765bb3c 100644 --- a/opteryx/models/__init__.py +++ b/opteryx/models/__init__.py @@ -10,7 +10,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from opteryx.compiled.functions.node import Node +from opteryx.compiled.structures.node import Node from opteryx.models.connection_context import ConnectionContext from opteryx.models.execution_tree import ExecutionTree from opteryx.models.logical_column import LogicalColumn diff --git a/opteryx/operators/distinct_node.py b/opteryx/operators/distinct_node.py index 2a23d8492..e74c0b3f0 100644 --- a/opteryx/operators/distinct_node.py +++ b/opteryx/operators/distinct_node.py @@ -29,7 +29,7 @@ class DistinctNode(BasePlanNode): def __init__(self, properties: QueryProperties, **config): - from opteryx.compiled.functions import HashSet + from opteryx.compiled.structures import HashSet super().__init__(properties=properties) self._distinct_on = config.get("on") @@ -51,7 +51,7 @@ def name(self): # pragma: no cover def execute(self) -> Generator[pyarrow.Table, None, None]: - from opteryx.compiled.functions import distinct + from opteryx.compiled.structures import distinct # We create a HashSet outside the distinct call, this allows us to pass # the hash to each run of the distinct which means we don't need to concat diff --git a/opteryx/operators/inner_join_node.py b/opteryx/operators/inner_join_node.py index 30d1cd181..a718514c1 100644 --- a/opteryx/operators/inner_join_node.py +++ b/opteryx/operators/inner_join_node.py @@ -35,7 +35,7 @@ import pyarrow -from opteryx.compiled.functions import HashTable +from opteryx.compiled.structures import HashTable from opteryx.models import QueryProperties from opteryx.operators import BasePlanNode from opteryx.utils.arrow import align_tables diff --git a/opteryx/operators/left_join_node.py b/opteryx/operators/left_join_node.py index 816651b1a..063796a3e 100644 --- a/opteryx/operators/left_join_node.py +++ b/opteryx/operators/left_join_node.py @@ -25,7 +25,7 @@ import pyarrow -from opteryx.compiled.functions import HashTable +from opteryx.compiled.structures import HashTable from opteryx.models import QueryProperties from opteryx.operators import BasePlanNode from opteryx.utils.arrow import align_tables diff --git a/opteryx/planner/logical_planner/logical_planner_builders.py b/opteryx/planner/logical_planner/logical_planner_builders.py index 6da26af3f..1dabfc1a2 100644 --- a/opteryx/planner/logical_planner/logical_planner_builders.py +++ b/opteryx/planner/logical_planner/logical_planner_builders.py @@ -524,9 +524,11 @@ def literal_string(branch, alias: Optional[List[str]] = None, key=None): def map_access(branch, alias: Optional[List[str]] = None, key=None): # Identifier[key] -> GET(Identifier, key) - identifier_node = build(branch["column"]) # ["Identifier"]["value"] - # key_dict = branch["keys"][0]["key"]["Value"] + identifier_node = build(branch["column"]) key_node = build(branch["keys"][0]["key"]) + key_value = key_node.value + if isinstance(key_value, str): + key_value = f"'{key_value}'" if key_node.node_type != NodeType.LITERAL: raise UnsupportedSyntaxError("Subscript values must be literals") @@ -535,8 +537,7 @@ def map_access(branch, alias: Optional[List[str]] = None, key=None): NodeType.FUNCTION, value="GET", parameters=[identifier_node, key_node], - alias=alias - or f"{identifier_node.current_name}[{repr(key) if isinstance(key, str) else key}]", + alias=alias or f"{identifier_node.current_name}[{key_value}]", ) diff --git a/setup.py b/setup.py index 339376ec2..736113b1a 100644 --- a/setup.py +++ b/setup.py @@ -85,8 +85,8 @@ def rust_build(setup_kwargs: Dict[str, Any]) -> None: extra_compile_args=COMPILE_FLAGS, ), Extension( - name="opteryx.compiled.functions.hash_table", - sources=["opteryx/compiled/functions/hash_table.pyx"], + name="opteryx.compiled.structures.hash_table", + sources=["opteryx/compiled/structures/hash_table.pyx"], include_dirs=[numpy.get_include()], language="c++", extra_compile_args=COMPILE_FLAGS + ["-std=c++11"], @@ -99,8 +99,8 @@ def rust_build(setup_kwargs: Dict[str, Any]) -> None: extra_compile_args=COMPILE_FLAGS + ["-std=c++11"], ), Extension( - name="opteryx.compiled.functions.node", - sources=["opteryx/compiled/functions/node.pyx"], + name="opteryx.compiled.structures.node", + sources=["opteryx/compiled/structures/node.pyx"], extra_compile_args=COMPILE_FLAGS, ), ] diff --git a/tests/sql_battery/test_shapes_and_errors_battery.py b/tests/sql_battery/test_shapes_and_errors_battery.py index 089e51f54..d35024ad3 100644 --- a/tests/sql_battery/test_shapes_and_errors_battery.py +++ b/tests/sql_battery/test_shapes_and_errors_battery.py @@ -1541,6 +1541,10 @@ # 1587 ("SELECT name, Mission_Status, Mission FROM $astronauts CROSS JOIN UNNEST (missions) AS mission_names INNER JOIN $missions ON Mission = mission_names WHERE mission_names = 'Apollo 11'", 3, 3, None), ("SELECT name, Mission_Status, Mission FROM $astronauts CROSS JOIN UNNEST (missions) AS mission_names INNER JOIN $missions ON Mission = mission_names WHERE Mission = 'Apollo 11'", 3, 3, None), + # 1607 + ("SELECT birth_place['town'], birth_place['state'] FROM $astronauts;", 357, 2, None), + ("SELECT birth_place->'town', birth_place->'state' FROM $astronauts;", 357, 2, None), + ("SELECT birth_place->>'town', birth_place->>'state' FROM $astronauts;", 357, 2, None), ] # fmt:on From f728bd2959d4b75d8d778a2473f7160b2ff54e88 Mon Sep 17 00:00:00 2001 From: XB500 Date: Thu, 25 Apr 2024 20:21:52 +0000 Subject: [PATCH 2/3] Opteryx Version 0.14.2-alpha.448 --- opteryx/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opteryx/__version__.py b/opteryx/__version__.py index b81d13404..4832a1ff1 100644 --- a/opteryx/__version__.py +++ b/opteryx/__version__.py @@ -1,4 +1,4 @@ -__build__ = 446 +__build__ = 448 # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From 432d25949f5cc4ce0facc221a94ef6d3f3afef36 Mon Sep 17 00:00:00 2001 From: XB500 Date: Thu, 25 Apr 2024 23:36:45 +0000 Subject: [PATCH 3/3] Opteryx Version 0.14.2-alpha.449 --- opteryx/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opteryx/__version__.py b/opteryx/__version__.py index 4832a1ff1..7ec2fc334 100644 --- a/opteryx/__version__.py +++ b/opteryx/__version__.py @@ -1,4 +1,4 @@ -__build__ = 448 +__build__ = 449 # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License.