├── tests ├── __init__.py ├── test_db.py ├── test_constraints.py ├── test_async_db.py ├── test_profile.py ├── test_edge.py ├── test_node.py ├── test_async_constraints.py ├── test_copy.py ├── test_async_profile.py ├── test_async_copy.py ├── test_explain.py ├── test_path.py ├── test_async_explain.py ├── test_indices.py ├── test_async_indices.py ├── test_graph.py └── test_async_graph.py ├── .gitignore ├── falkordb ├── asyncio │ ├── __init__.py │ ├── cluster.py │ ├── graph_schema.py │ ├── falkordb.py │ └── query_result.py ├── __init__.py ├── exceptions.py ├── helpers.py ├── sentinel.py ├── cluster.py ├── node.py ├── graph_schema.py ├── edge.py ├── path.py ├── execution_plan.py ├── falkordb.py └── query_result.py ├── pytest.ini ├── docs ├── source │ ├── modules.rst │ ├── index.rst │ ├── falkordb.asyncio.rst │ ├── conf.py │ └── falkordb.rst ├── requirements.txt ├── Makefile └── make.bat ├── .github ├── wordlist.txt ├── workflows │ ├── pypi-publish.yaml │ ├── spellcheck.yml │ └── test.yml ├── dependabot.yml └── spellcheck-settings.yml ├── .readthedocs.yaml ├── LICENSE ├── pyproject.toml ├── README.md └── CODE_OF_CONDUCT.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | __pycache__ 3 | poetry.lock -------------------------------------------------------------------------------- /falkordb/asyncio/__init__.py: -------------------------------------------------------------------------------- 1 | from .falkordb import FalkorDB 2 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --asyncio-mode=auto 3 | 4 | -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | falkordb 2 | ======== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | falkordb 8 | -------------------------------------------------------------------------------- /.github/wordlist.txt: -------------------------------------------------------------------------------- 1 | falkordb 2 | FalkorDB 3 | py 4 | socio 5 | sexualized 6 | html 7 | https 8 | www 9 | faq 10 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | redis 2 | sphinx_rtd_theme==3.0.2 3 | requests>=2.32.2 # not directly required, pinned by Snyk to avoid a vulnerability 4 | urllib3>=2.2.2 # not directly required, pinned by Snyk to avoid a vulnerability 5 | zipp>=3.19.1 # not directly required, pinned by Snyk to avoid a vulnerability 6 | -------------------------------------------------------------------------------- /falkordb/__init__.py: -------------------------------------------------------------------------------- 1 | from .falkordb import FalkorDB 2 | from .node import Node 3 | from .edge import Edge 4 | from .path import Path 5 | from .graph import Graph 6 | from .execution_plan import ExecutionPlan, Operation 7 | from .query_result import QueryResult 8 | -------------------------------------------------------------------------------- /.github/workflows/pypi-publish.yaml: -------------------------------------------------------------------------------- 1 | # Run this job on tagging 2 | name: Release to PYPI 3 | permissions: 4 | contents: read 5 | on: 6 | push: 7 | tags: 8 | - "v*.*.*" 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v6 14 | - name: Build and publish to pypi 15 | uses: JRubics/poetry-publish@v2.1 16 | with: 17 | pypi_token: ${{ secrets.PYPI_API_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/spellcheck.yml: -------------------------------------------------------------------------------- 1 | name: Spellcheck 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | jobs: 8 | spellcheck: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | steps: 13 | - uses: actions/checkout@v6 14 | - name: Spellcheck 15 | uses: rojopolis/spellcheck-github-actions@0.55.0 16 | with: 17 | config_path: .github/spellcheck-settings.yml 18 | task_name: Markdown 19 | -------------------------------------------------------------------------------- /falkordb/exceptions.py: -------------------------------------------------------------------------------- 1 | class SchemaVersionMismatchException(Exception): 2 | """ 3 | Exception raised when the schema version of the database does not match the 4 | version of the schema that the application expects. 5 | """ 6 | def __init__(self, version: int): 7 | """ 8 | Create a new SchemaVersionMismatchException. 9 | 10 | Args: 11 | version: The version of the schema that the application expects. 12 | 13 | """ 14 | 15 | self.version = version 16 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. FalkorDB-py documentation master file, created by 2 | sphinx-quickstart on Sun Nov 19 15:57:26 2023. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to FalkorDB-py's documentation! 7 | ======================================= 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | modules 14 | 15 | Indices and tables 16 | ================== 17 | 18 | * :ref:`genindex` 19 | * :ref:`modindex` 20 | * :ref:`search` 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | -------------------------------------------------------------------------------- /.github/spellcheck-settings.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | - name: Markdown 3 | expect_match: false 4 | apsell: 5 | lang: en 6 | d: en_US 7 | dictionary: 8 | wordlists: 9 | - .github/wordlist.txt 10 | output: wordlist.dic 11 | pipeline: 12 | - pyspelling.filters.markdown: 13 | markdown_extensions: 14 | - markdown.extensions.extra: 15 | - pyspelling.filters.html: 16 | comments: false 17 | attributes: 18 | - alt 19 | ignores: 20 | - ':matches(code, pre)' 21 | - code 22 | - pre 23 | - blockquote 24 | - img 25 | - table 26 | sources: 27 | - '*.md' 28 | - 'docs/*.md' 29 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # # Read the Docs configuration file 3 | # # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | # 5 | # Required 6 | version: 2 7 | 8 | # Set the OS, Python version and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.12" 13 | # You can also specify other tool versions: 14 | # nodejs: "19" 15 | # rust: "1.64" 16 | # golang: "1.19" 17 | 18 | # Build documentation in the "docs/" directory with Sphinx 19 | sphinx: 20 | configuration: docs/source/conf.py 21 | 22 | # Optionally build your docs in additional formats such as PDF and ePub 23 | # formats: 24 | # - pdf 25 | # - epub 26 | 27 | # Optional but recommended, declare the Python requirements required 28 | # to build your documentation 29 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 30 | python: 31 | install: 32 | - requirements: docs/requirements.txt 33 | -------------------------------------------------------------------------------- /docs/source/falkordb.asyncio.rst: -------------------------------------------------------------------------------- 1 | falkordb.asyncio package 2 | ======================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | falkordb.asyncio.falkordb module 8 | -------------------------------- 9 | 10 | .. automodule:: falkordb.asyncio.falkordb 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | falkordb.asyncio.graph module 16 | ----------------------------- 17 | 18 | .. automodule:: falkordb.asyncio.graph 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | falkordb.asyncio.graph\_schema module 24 | ------------------------------------- 25 | 26 | .. automodule:: falkordb.asyncio.graph_schema 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | falkordb.asyncio.query\_result module 32 | ------------------------------------- 33 | 34 | .. automodule:: falkordb.asyncio.query_result 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | Module contents 40 | --------------- 41 | 42 | .. automodule:: falkordb.asyncio 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 FalkorDB 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "FalkorDB" 3 | version = "1.2.2" 4 | description = "Python client for interacting with FalkorDB database" 5 | authors = ["FalkorDB inc "] 6 | 7 | readme = "README.md" 8 | repository = "http://github.com/falkorDB/falkordb-py" 9 | homepage = "http://falkordb-py.readthedocs.io" 10 | keywords = ['FalkorDB', 'GraphDB', 'Cypher'] 11 | classifiers = [ 12 | "Programming Language :: Python :: 3", 13 | "Programming Language :: Python :: 3.8", 14 | "Programming Language :: Python :: 3.9", 15 | "Programming Language :: Python :: 3.10", 16 | "Programming Language :: Python :: 3.11", 17 | "Programming Language :: Python :: 3.12", 18 | "Programming Language :: Python :: 3.13", 19 | "Programming Language :: Python :: 3.14", 20 | ] 21 | 22 | packages = [{include = "falkordb"}] 23 | 24 | [tool.poetry.dependencies] 25 | python = "^3.8" 26 | redis = ">=6.0.0,<7.0.0" 27 | python-dateutil = "^2.9.0" 28 | 29 | [tool.poetry.group.test.dependencies] 30 | pytest-cov = ">=4.1,<6.0" 31 | pytest = "8.3.5" 32 | pytest-asyncio = ">=0.23.4,<0.25.0" 33 | 34 | [build-system] 35 | requires = ["poetry-core"] 36 | build-backend = "poetry.core.masonry.api" 37 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Path setup -------------------------------------------------------------- 7 | 8 | import os 9 | import sys 10 | sys.path.insert(0, os.path.abspath('../..')) 11 | 12 | # -- Project information ----------------------------------------------------- 13 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 14 | 15 | project = 'FalkorDB-py' 16 | copyright = '2023, FalkorDB inc' 17 | author = 'FalkorDB inc' 18 | 19 | # -- General configuration --------------------------------------------------- 20 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 21 | 22 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] 23 | 24 | templates_path = ['_templates'] 25 | exclude_patterns = [] 26 | 27 | 28 | 29 | # -- Options for HTML output ------------------------------------------------- 30 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 31 | 32 | html_theme = 'sphinx_rtd_theme' 33 | html_static_path = ['_static'] 34 | -------------------------------------------------------------------------------- /falkordb/helpers.py: -------------------------------------------------------------------------------- 1 | def quote_string(v): 2 | """ 3 | FalkorDB strings must be quoted, 4 | quote_string wraps given v with quotes incase 5 | v is a string. 6 | """ 7 | 8 | if isinstance(v, bytes): 9 | v = v.decode() 10 | elif not isinstance(v, str): 11 | return v 12 | if len(v) == 0: 13 | return '""' 14 | 15 | v = v.replace("\\", "\\\\") 16 | v = v.replace('"', '\\"') 17 | 18 | return f'"{v}"' 19 | 20 | def stringify_param_value(value): 21 | """ 22 | turn a parameter value into a string suitable for the params header of 23 | a Cypher command 24 | you may pass any value that would be accepted by `json.dumps()` 25 | 26 | ways in which output differs from that of `str()`: 27 | * strings are quoted 28 | * None --> "null" 29 | * in dictionaries, keys are _not_ quoted 30 | 31 | :param value: the parameter value to be turned into a string 32 | :return: string 33 | """ 34 | 35 | if isinstance(value, str): 36 | return quote_string(value) 37 | 38 | if value is None: 39 | return "null" 40 | 41 | if isinstance(value, (list, tuple)): 42 | return f'[{",".join(map(stringify_param_value, value))}]' 43 | 44 | if isinstance(value, dict): 45 | return f'{{{",".join(f"{k}:{stringify_param_value(v)}" for k, v in value.items())}}}' # noqa 46 | 47 | return str(value) 48 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | permissions: 3 | contents: read 4 | on: 5 | push: 6 | branches: [main] 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | name: Test with Python ${{ matrix.python }} 12 | runs-on: ubuntu-latest 13 | 14 | services: 15 | falkordb: 16 | # Docker Hub image 17 | image: falkordb/falkordb:edge 18 | # Map port 6379 on the Docker host to port 6379 on the FalkorDB container 19 | ports: 20 | - 6379:6379 21 | 22 | strategy: 23 | matrix: 24 | python: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] 25 | fail-fast: false 26 | 27 | steps: 28 | - uses: actions/checkout@v6 29 | 30 | - uses: actions/setup-python@v6 31 | with: 32 | python-version: ${{matrix.python}} 33 | 34 | - uses: snok/install-poetry@v1 35 | with: 36 | version: 1.7.1 37 | virtualenvs-create: true 38 | virtualenvs-in-project: true 39 | 40 | - name: Install dependencies 41 | run: poetry install --no-interaction 42 | 43 | - name: Run Tests 44 | run: poetry run pytest --cov --cov-report=xml 45 | 46 | - name: Upload coverage 47 | uses: codecov/codecov-action@v5 48 | if: matrix.python == '3.10' && matrix.platform != 'macos-11' 49 | with: 50 | fail_ci_if_error: false 51 | token: ${{ secrets.CODECOV_TOKEN }} 52 | -------------------------------------------------------------------------------- /tests/test_db.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from falkordb import FalkorDB 3 | 4 | 5 | @pytest.fixture 6 | def client(request): 7 | return FalkorDB(host='localhost', port=6379) 8 | 9 | 10 | def test_config(client): 11 | db = client 12 | config_name = "RESULTSET_SIZE" 13 | 14 | # save olf configuration value 15 | prev_value = int(db.config_get(config_name)) 16 | 17 | # set configuration 18 | response = db.config_set(config_name, 3) 19 | assert response == "OK" 20 | 21 | # make sure config been updated 22 | new_value = int(db.config_get(config_name)) 23 | assert new_value == 3 24 | 25 | # restore original value 26 | response = db.config_set(config_name, prev_value) 27 | assert response == "OK" 28 | 29 | # trying to get / set invalid configuration 30 | with pytest.raises(Exception): 31 | db.config_get("none_existing_conf") 32 | 33 | with pytest.raises(Exception): 34 | db.config_set("none_existing_conf", 1) 35 | 36 | with pytest.raises(Exception): 37 | db.config_set(config_name, "invalid value") 38 | 39 | def test_connect_via_url(): 40 | # make sure we're able to connect via url 41 | 42 | # just host 43 | db = FalkorDB.from_url("falkor://localhost") 44 | g = db.select_graph("db") 45 | qr = g.query("RETURN 1") 46 | one = qr.result_set[0][0] 47 | assert one == 1 48 | 49 | # host & Port 50 | db = FalkorDB.from_url("falkor://localhost:6379") 51 | g = db.select_graph("db") 52 | qr = g.query("RETURN 1") 53 | one = qr.result_set[0][0] 54 | header = qr.header 55 | assert one == 1 56 | assert header[0][0] == 1 57 | assert header[0][1] == '1' 58 | -------------------------------------------------------------------------------- /tests/test_constraints.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from redis import ResponseError 3 | from falkordb import FalkorDB 4 | 5 | def test_constraints(): 6 | db = FalkorDB(host='localhost', port=6379) 7 | g = db.select_graph("constraints") 8 | 9 | # create node constraints 10 | g.create_node_unique_constraint("Person", "name") 11 | g.create_node_mandatory_constraint("Person", "name") 12 | g.create_node_unique_constraint("Person", "v1", "v2") 13 | 14 | # create edge constraints 15 | g.create_edge_unique_constraint("KNOWS", "since") 16 | g.create_edge_mandatory_constraint("KNOWS", "since") 17 | g.create_edge_unique_constraint("KNOWS", "v1", "v2") 18 | 19 | constraints = g.list_constraints() 20 | assert(len(constraints) == 6) 21 | 22 | # drop constraints 23 | g.drop_node_unique_constraint("Person", "name") 24 | g.drop_node_mandatory_constraint("Person", "name") 25 | g.drop_node_unique_constraint("Person", "v1", "v2") 26 | 27 | g.drop_edge_unique_constraint("KNOWS", "since") 28 | g.drop_edge_mandatory_constraint("KNOWS", "since") 29 | g.drop_edge_unique_constraint("KNOWS", "v1", "v2") 30 | 31 | constraints = g.list_constraints() 32 | assert(len(constraints) == 0) 33 | 34 | def test_create_existing_constraint(): 35 | # trying to create an existing constraint 36 | db = FalkorDB(host='localhost', port=6379) 37 | g = db.select_graph("constraints") 38 | 39 | # create node constraints 40 | g.create_node_unique_constraint("Person", "name") 41 | try: 42 | g.create_node_unique_constraint("Person", "name") 43 | assert(False) 44 | except Exception as e: 45 | assert("Constraint already exists" == str(e)) 46 | 47 | -------------------------------------------------------------------------------- /tests/test_async_db.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import asyncio 3 | from falkordb.asyncio import FalkorDB 4 | from redis.asyncio import BlockingConnectionPool 5 | 6 | @pytest.mark.asyncio 7 | async def test_config(): 8 | pool = BlockingConnectionPool(max_connections=16, timeout=None, decode_responses=True) 9 | db = FalkorDB(connection_pool=pool) 10 | config_name = "RESULTSET_SIZE" 11 | 12 | # save olf configuration value 13 | prev_value = int(await db.config_get(config_name)) 14 | 15 | # set configuration 16 | response = await db.config_set(config_name, 3) 17 | assert response == "OK" 18 | 19 | # make sure config been updated 20 | new_value = int(await db.config_get(config_name)) 21 | assert new_value == 3 22 | 23 | # restore original value 24 | response = await db.config_set(config_name, prev_value) 25 | assert response == "OK" 26 | 27 | # trying to get / set invalid configuration 28 | with pytest.raises(Exception): 29 | await db.config_get("none_existing_conf") 30 | 31 | with pytest.raises(Exception): 32 | await db.config_set("none_existing_conf", 1) 33 | 34 | with pytest.raises(Exception): 35 | await db.config_set(config_name, "invalid value") 36 | 37 | # close the connection pool 38 | await pool.aclose() 39 | 40 | @pytest.mark.asyncio 41 | async def test_connect_via_url(): 42 | pool = BlockingConnectionPool(max_connections=16, timeout=None, decode_responses=True) 43 | db = FalkorDB(connection_pool=pool) 44 | 45 | # make sure we're able to connect via url 46 | g = db.select_graph("async_db") 47 | one = (await g.query("RETURN 1")).result_set[0][0] 48 | assert one == 1 49 | 50 | # close the connection pool 51 | await pool.aclose() 52 | -------------------------------------------------------------------------------- /falkordb/sentinel.py: -------------------------------------------------------------------------------- 1 | from redis.sentinel import Sentinel 2 | 3 | # detect if a connection is a sentinel 4 | def Is_Sentinel(conn): 5 | info = conn.info(section="server") 6 | return "redis_mode" in info and info["redis_mode"] == "sentinel" 7 | 8 | # create a sentinel connection from a Redis connection 9 | def Sentinel_Conn(conn, ssl): 10 | # collect masters 11 | masters = conn.sentinel_masters() 12 | 13 | # abort if multiple masters are detected 14 | if len(masters) != 1: 15 | raise Exception("Multiple masters, require service name") 16 | 17 | # monitored service name 18 | service_name = list(masters.keys())[0] 19 | 20 | # list of sentinels connection information 21 | sentinels_conns = [] 22 | 23 | # current sentinel 24 | host = conn.connection_pool.connection_kwargs['host'] 25 | port = conn.connection_pool.connection_kwargs['port'] 26 | sentinels_conns.append((host, port)) 27 | 28 | # additional sentinels 29 | #sentinels = conn.sentinel_sentinels(service_name) 30 | #for sentinel in sentinels: 31 | # ip = sentinel['ip'] 32 | # port = sentinel['port'] 33 | # sentinels_conns.append((host, port)) 34 | 35 | # use the same connection arguments e.g. username and password 36 | connection_kwargs = conn.connection_pool.connection_kwargs 37 | 38 | # construct sentinel arguments 39 | sentinel_kwargs = { } 40 | if 'username' in connection_kwargs: 41 | sentinel_kwargs['username'] = connection_kwargs['username'] 42 | if 'password' in connection_kwargs: 43 | sentinel_kwargs['password'] = connection_kwargs['password'] 44 | if ssl: 45 | sentinel_kwargs['ssl'] = True 46 | 47 | return (Sentinel(sentinels_conns, sentinel_kwargs=sentinel_kwargs, **connection_kwargs), service_name) 48 | 49 | -------------------------------------------------------------------------------- /tests/test_profile.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from falkordb import FalkorDB 3 | 4 | 5 | @pytest.fixture 6 | def client(request): 7 | db = FalkorDB(host='localhost', port=6379) 8 | return db.select_graph("profile") 9 | 10 | 11 | def test_profile(client): 12 | g = client 13 | plan = g.profile("UNWIND range(0, 3) AS x RETURN x") 14 | 15 | results_op = plan.structured_plan 16 | assert(results_op.name == 'Results') 17 | assert(len(results_op.children) == 1) 18 | assert(results_op.profile_stats.records_produced == 4) 19 | 20 | project_op = results_op.children[0] 21 | assert(project_op.name == 'Project') 22 | assert(len(project_op.children) == 1) 23 | assert(project_op.profile_stats.records_produced == 4) 24 | 25 | unwind_op = project_op.children[0] 26 | assert(unwind_op.name == 'Unwind') 27 | assert(len(unwind_op.children) == 0) 28 | assert(unwind_op.profile_stats.records_produced == 4) 29 | 30 | def test_cartesian_product_profile(client): 31 | g = client 32 | plan = g.profile("MATCH (a), (b) RETURN *") 33 | 34 | results_op = plan.structured_plan 35 | assert(results_op.name == 'Results') 36 | assert(len(results_op.children) == 1) 37 | assert(results_op.profile_stats.records_produced == 0) 38 | 39 | project_op = results_op.children[0] 40 | assert(project_op.name == 'Project') 41 | assert(len(project_op.children) == 1) 42 | assert(project_op.profile_stats.records_produced == 0) 43 | 44 | cp_op = project_op.children[0] 45 | assert(cp_op.name == 'Cartesian Product') 46 | assert(len(cp_op.children) == 2) 47 | assert(cp_op.profile_stats.records_produced == 0) 48 | 49 | scan_a_op = cp_op.children[0] 50 | scan_b_op = cp_op.children[1] 51 | 52 | assert(scan_a_op.name == 'All Node Scan') 53 | assert(len(scan_a_op.children) == 0) 54 | assert(scan_a_op.profile_stats.records_produced == 0) 55 | 56 | assert(scan_b_op.name == 'All Node Scan') 57 | assert(len(scan_b_op.children) == 0) 58 | assert(scan_b_op.profile_stats.records_produced == 0) 59 | -------------------------------------------------------------------------------- /tests/test_edge.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from falkordb import Node, Edge 3 | 4 | def test_init(): 5 | with pytest.raises(AssertionError): 6 | Edge(None, None, None) 7 | Edge(Node(), None, None) 8 | Edge(None, None, Node()) 9 | 10 | assert isinstance( 11 | Edge(Node(node_id=1), None, Node(node_id=2)), Edge 12 | ) 13 | 14 | 15 | def test_to_string(): 16 | props_result = Edge( 17 | Node(), None, Node(), properties={"a": "a", "b": 10} 18 | ).to_string() 19 | assert props_result == '{a:"a",b:10}' 20 | 21 | no_props_result = Edge( 22 | Node(), None, Node(), properties={} 23 | ).to_string() 24 | assert no_props_result == "" 25 | 26 | 27 | def test_stringify(): 28 | john = Node( 29 | alias="a", 30 | labels="person", 31 | properties={"name": 'John Doe', "age": 33, "someArray": [1, 2, 3]}, 32 | ) 33 | 34 | japan = Node(alias="b", 35 | labels="country", 36 | properties={"name": 'Japan'} 37 | ) 38 | 39 | edge_with_relation = Edge( 40 | john, 41 | "visited", 42 | japan, 43 | properties={"purpose": "pleasure"} 44 | ) 45 | assert("(a)-[:visited{purpose:\"pleasure\"}]->(b)" == str(edge_with_relation)) 46 | 47 | edge_no_relation_no_props = Edge(japan, "", john) 48 | assert("(b)-[]->(a)" == str(edge_no_relation_no_props)) 49 | 50 | edge_only_props = Edge(john, "", japan, properties={"a": "b", "c": 3}) 51 | assert("(a)-[{a:\"b\",c:3}]->(b)" == str(edge_only_props)) 52 | 53 | 54 | def test_comparision(): 55 | node1 = Node(node_id=1) 56 | node2 = Node(node_id=2) 57 | node3 = Node(node_id=3) 58 | 59 | edge1 = Edge(node1, None, node2) 60 | assert edge1 == Edge(node1, None, node2) 61 | assert edge1 != Edge(node1, "bla", node2) 62 | assert edge1 != Edge(node1, None, node3) 63 | assert edge1 != Edge(node3, None, node2) 64 | assert edge1 != Edge(node2, None, node1) 65 | assert edge1 != Edge(node1, None, node2, properties={"a": 10}) 66 | -------------------------------------------------------------------------------- /falkordb/cluster.py: -------------------------------------------------------------------------------- 1 | from redis.cluster import RedisCluster 2 | import redis.exceptions as redis_exceptions 3 | import socket 4 | 5 | # detect if a connection is a Cluster 6 | def Is_Cluster(conn): 7 | info = conn.info(section="server") 8 | return "redis_mode" in info and info["redis_mode"] == "cluster" 9 | 10 | 11 | # create a cluster connection from a Redis connection 12 | def Cluster_Conn( 13 | conn, 14 | ssl, 15 | cluster_error_retry_attempts=3, 16 | startup_nodes=None, 17 | require_full_coverage=False, 18 | reinitialize_steps=5, 19 | read_from_replicas=False, 20 | dynamic_startup_nodes=True, 21 | url=None, 22 | address_remap=None, 23 | ): 24 | connection_kwargs = conn.connection_pool.connection_kwargs 25 | host = connection_kwargs.pop("host") 26 | port = connection_kwargs.pop("port") 27 | username = connection_kwargs.pop("username") 28 | password = connection_kwargs.pop("password") 29 | 30 | retry = connection_kwargs.pop("retry", None) 31 | retry_on_timeout = connection_kwargs.pop("retry_on_timeout", None) 32 | retry_on_error = connection_kwargs.pop( 33 | "retry_on_error", 34 | [ 35 | ConnectionRefusedError, 36 | ConnectionError, 37 | TimeoutError, 38 | socket.timeout, 39 | redis_exceptions.ConnectionError, 40 | ], 41 | ) 42 | return RedisCluster( 43 | host=host, 44 | port=port, 45 | username=username, 46 | password=password, 47 | decode_responses=True, 48 | ssl=ssl, 49 | retry=retry, 50 | retry_on_timeout=retry_on_timeout, 51 | retry_on_error=retry_on_error, 52 | require_full_coverage=require_full_coverage, 53 | reinitialize_steps=reinitialize_steps, 54 | read_from_replicas=read_from_replicas, 55 | dynamic_startup_nodes=dynamic_startup_nodes, 56 | url=url, 57 | address_remap=address_remap, 58 | startup_nodes=startup_nodes, 59 | cluster_error_retry_attempts=cluster_error_retry_attempts, 60 | ) 61 | -------------------------------------------------------------------------------- /tests/test_node.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from falkordb import Node 3 | 4 | 5 | @pytest.fixture 6 | def fixture(): 7 | no_args = Node(alias="n") 8 | no_props = Node(node_id=1, alias="n", labels="l") 9 | no_label = Node(node_id=1, alias="n", properties={"a": "a"}) 10 | props_only = Node(alias="n", properties={"a": "a", "b": 10}) 11 | multi_label = Node(node_id=1, alias="n", labels=["l", "ll"]) 12 | 13 | return no_args, no_props, props_only, no_label, multi_label 14 | 15 | 16 | def test_to_string(fixture): 17 | no_args, no_props, props_only, no_label, multi_label = fixture 18 | 19 | assert no_args.to_string() == "" 20 | assert no_props.to_string() == "" 21 | assert no_label.to_string() == '{a:"a"}' 22 | assert props_only.to_string() == '{a:"a",b:10}' 23 | assert multi_label.to_string() == "" 24 | 25 | 26 | def test_stringify(fixture): 27 | no_args, no_props, props_only, no_label, multi_label = fixture 28 | 29 | assert str(no_args) == "(n)" 30 | assert str(no_props) == "(n:l)" 31 | assert str(no_label) == '(n{a:"a"})' 32 | assert str(props_only) == '(n{a:"a",b:10})' 33 | assert str(multi_label) == "(n:l:ll)" 34 | 35 | 36 | def test_comparision(): 37 | assert Node() != Node(properties={"a": 10}) 38 | assert Node() == Node() 39 | assert Node(node_id=1) == Node(node_id=1) 40 | assert Node(node_id=1) != Node(node_id=2) 41 | assert Node(node_id=1, alias="a") == Node(node_id=1, alias="b") 42 | assert Node(node_id=1, alias="a") == Node(node_id=1, alias="a") 43 | assert Node(node_id=1, labels="a") == Node(node_id=1, labels="a") 44 | assert Node(node_id=1, labels="a") != Node(node_id=1, labels="b") 45 | assert Node(alias="a", labels="l") != Node(alias="a", labels="l1") 46 | assert Node(properties={"a": 10}) == Node(properties={"a": 10}) 47 | assert Node(node_id=1, alias="a", labels="l") == Node(node_id=1, alias="a", labels="l") 48 | -------------------------------------------------------------------------------- /docs/source/falkordb.rst: -------------------------------------------------------------------------------- 1 | falkordb package 2 | ================ 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | :maxdepth: 4 9 | 10 | falkordb.asyncio 11 | 12 | Submodules 13 | ---------- 14 | 15 | falkordb.edge module 16 | -------------------- 17 | 18 | .. automodule:: falkordb.edge 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | falkordb.exceptions module 24 | -------------------------- 25 | 26 | .. automodule:: falkordb.exceptions 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | falkordb.execution\_plan module 32 | ------------------------------- 33 | 34 | .. automodule:: falkordb.execution_plan 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | falkordb.falkordb module 40 | ------------------------ 41 | 42 | .. automodule:: falkordb.falkordb 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | falkordb.graph module 48 | --------------------- 49 | 50 | .. automodule:: falkordb.graph 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | falkordb.graph\_schema module 56 | ----------------------------- 57 | 58 | .. automodule:: falkordb.graph_schema 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | falkordb.helpers module 64 | ----------------------- 65 | 66 | .. automodule:: falkordb.helpers 67 | :members: 68 | :undoc-members: 69 | :show-inheritance: 70 | 71 | falkordb.node module 72 | -------------------- 73 | 74 | .. automodule:: falkordb.node 75 | :members: 76 | :undoc-members: 77 | :show-inheritance: 78 | 79 | falkordb.path module 80 | -------------------- 81 | 82 | .. automodule:: falkordb.path 83 | :members: 84 | :undoc-members: 85 | :show-inheritance: 86 | 87 | falkordb.query\_result module 88 | ----------------------------- 89 | 90 | .. automodule:: falkordb.query_result 91 | :members: 92 | :undoc-members: 93 | :show-inheritance: 94 | 95 | Module contents 96 | --------------- 97 | 98 | .. automodule:: falkordb 99 | :members: 100 | :undoc-members: 101 | :show-inheritance: 102 | -------------------------------------------------------------------------------- /tests/test_async_constraints.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import asyncio 3 | from falkordb.asyncio import FalkorDB 4 | from redis.asyncio import BlockingConnectionPool 5 | 6 | @pytest.mark.asyncio 7 | async def test_constraints(): 8 | pool = BlockingConnectionPool(max_connections=16, timeout=None, decode_responses=True) 9 | db = FalkorDB(connection_pool=pool) 10 | g = db.select_graph("async_constraints") 11 | 12 | # create node constraints 13 | await g.create_node_unique_constraint("Person", "name") 14 | await g.create_node_mandatory_constraint("Person", "name") 15 | await g.create_node_unique_constraint("Person", "v1", "v2") 16 | 17 | # create edge constraints 18 | await g.create_edge_unique_constraint("KNOWS", "since") 19 | await g.create_edge_mandatory_constraint("KNOWS", "since") 20 | await g.create_edge_unique_constraint("KNOWS", "v1", "v2") 21 | 22 | constraints = await g.list_constraints() 23 | assert(len(constraints) == 6) 24 | 25 | # drop constraints 26 | await g.drop_node_unique_constraint("Person", "name") 27 | await g.drop_node_mandatory_constraint("Person", "name") 28 | await g.drop_node_unique_constraint("Person", "v1", "v2") 29 | 30 | await g.drop_edge_unique_constraint("KNOWS", "since") 31 | await g.drop_edge_mandatory_constraint("KNOWS", "since") 32 | await g.drop_edge_unique_constraint("KNOWS", "v1", "v2") 33 | 34 | constraints = await g.list_constraints() 35 | assert(len(constraints) == 0) 36 | 37 | # close the connection pool 38 | await pool.aclose() 39 | 40 | @pytest.mark.asyncio 41 | async def test_create_existing_constraint(): 42 | # trying to create an existing constraint 43 | pool = BlockingConnectionPool(max_connections=16, timeout=None, decode_responses=True) 44 | db = FalkorDB(connection_pool=pool) 45 | g = db.select_graph("async_constraints") 46 | 47 | # create node constraints 48 | await g.create_node_unique_constraint("Person", "name") 49 | try: 50 | await g.create_node_unique_constraint("Person", "name") 51 | assert(False) 52 | except Exception as e: 53 | assert("Constraint already exists" == str(e)) 54 | 55 | # close the connection pool 56 | await pool.aclose() 57 | -------------------------------------------------------------------------------- /tests/test_copy.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from falkordb import FalkorDB 3 | 4 | def test_graph_copy(): 5 | # create a simple graph and clone it 6 | # make sure graphs are the same 7 | 8 | db = FalkorDB(host='localhost', port=6379) 9 | src = db.select_graph("copy_src") 10 | 11 | # create entities 12 | src.query("CREATE (:A {v:1})-[:R {v:2}]->(:B {v:3})") 13 | 14 | # create index 15 | src.create_edge_range_index("A", "v") 16 | src.create_edge_range_index("R", "v") 17 | src.create_node_fulltext_index("B", "v") 18 | 19 | # create constrain 20 | src.create_node_unique_constraint("A", "v") 21 | src.create_edge_unique_constraint("R", "v") 22 | 23 | # clone graph 24 | dest = src.copy("copy_dest") 25 | 26 | # validate src and dest are the same 27 | # validate entities 28 | q = "MATCH (a) RETURN a ORDER BY ID(a)" 29 | src_res = src.query(q).result_set 30 | dest_res = dest.query(q).result_set 31 | assert(src_res == dest_res) 32 | 33 | q = "MATCH ()-[e]->() RETURN e ORDER BY ID(e)" 34 | src_res = src.query(q).result_set 35 | dest_res = dest.query(q).result_set 36 | assert(src_res == dest_res) 37 | 38 | # validate schema 39 | src_res = src.call_procedure("DB.LABELS").result_set 40 | dest_res = dest.call_procedure("DB.LABELS").result_set 41 | assert(src_res == dest_res) 42 | 43 | src_res = src.call_procedure("DB.PROPERTYKEYS").result_set 44 | dest_res = dest.call_procedure("DB.PROPERTYKEYS").result_set 45 | assert(src_res == dest_res) 46 | 47 | src_res = src.call_procedure("DB.RELATIONSHIPTYPES").result_set 48 | dest_res = dest.call_procedure("DB.RELATIONSHIPTYPES").result_set 49 | assert(src_res == dest_res) 50 | 51 | # validate indices 52 | q = """CALL DB.INDEXES() 53 | YIELD label, properties, types, language, stopwords, entitytype, status 54 | RETURN * 55 | ORDER BY label, properties, types, language, stopwords, entitytype, status""" 56 | src_res = src.query(q).result_set 57 | dest_res = dest.query(q).result_set 58 | 59 | assert(src_res == dest_res) 60 | 61 | # validate constraints 62 | src_res = src.list_constraints() 63 | dest_res = dest.list_constraints() 64 | assert(src_res == dest_res) 65 | -------------------------------------------------------------------------------- /falkordb/asyncio/cluster.py: -------------------------------------------------------------------------------- 1 | from redis.asyncio.cluster import RedisCluster 2 | import redis.exceptions as redis_exceptions 3 | import redis.asyncio as redis 4 | import redis as sync_redis 5 | import socket 6 | 7 | 8 | # detect if a connection is a cluster 9 | def Is_Cluster(conn: redis.Redis): 10 | 11 | pool = conn.connection_pool 12 | kwargs = pool.connection_kwargs.copy() 13 | 14 | # Check if the connection is using SSL and add it 15 | # this propery is not kept in the connection_kwargs 16 | kwargs["ssl"] = pool.connection_class is redis.SSLConnection 17 | 18 | # Create a synchronous Redis client with the same parameters 19 | # as the connection pool just to keep Is_Cluster synchronous 20 | info = sync_redis.Redis(**kwargs).info(section="server") 21 | 22 | return "redis_mode" in info and info["redis_mode"] == "cluster" 23 | 24 | 25 | # create a cluster connection from a Redis connection 26 | def Cluster_Conn( 27 | conn, 28 | ssl, 29 | cluster_error_retry_attempts=3, 30 | startup_nodes=None, 31 | require_full_coverage=False, 32 | reinitialize_steps=5, 33 | read_from_replicas=False, 34 | address_remap=None, 35 | ): 36 | connection_kwargs = conn.connection_pool.connection_kwargs 37 | host = connection_kwargs.pop("host") 38 | port = connection_kwargs.pop("port") 39 | username = connection_kwargs.pop("username") 40 | password = connection_kwargs.pop("password") 41 | 42 | retry = connection_kwargs.pop("retry", None) 43 | retry_on_error = connection_kwargs.pop( 44 | "retry_on_error", 45 | [ 46 | ConnectionRefusedError, 47 | ConnectionError, 48 | TimeoutError, 49 | socket.timeout, 50 | redis_exceptions.ConnectionError, 51 | ], 52 | ) 53 | return RedisCluster( 54 | host=host, 55 | port=port, 56 | username=username, 57 | password=password, 58 | decode_responses=True, 59 | ssl=ssl, 60 | retry=retry, 61 | retry_on_error=retry_on_error, 62 | require_full_coverage=require_full_coverage, 63 | reinitialize_steps=reinitialize_steps, 64 | read_from_replicas=read_from_replicas, 65 | address_remap=address_remap, 66 | startup_nodes=startup_nodes, 67 | cluster_error_retry_attempts=cluster_error_retry_attempts, 68 | ) 69 | -------------------------------------------------------------------------------- /tests/test_async_profile.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from falkordb.asyncio import FalkorDB 3 | from redis.asyncio import BlockingConnectionPool 4 | 5 | 6 | @pytest.mark.asyncio 7 | async def test_profile(): 8 | pool = BlockingConnectionPool(max_connections=16, timeout=None, decode_responses=True) 9 | db = FalkorDB(connection_pool=pool) 10 | g = db.select_graph("async_profile") 11 | 12 | plan = await g.profile("UNWIND range(0, 3) AS x RETURN x") 13 | 14 | results_op = plan.structured_plan 15 | assert(results_op.name == 'Results') 16 | assert(len(results_op.children) == 1) 17 | assert(results_op.profile_stats.records_produced == 4) 18 | 19 | project_op = results_op.children[0] 20 | assert(project_op.name == 'Project') 21 | assert(len(project_op.children) == 1) 22 | assert(project_op.profile_stats.records_produced == 4) 23 | 24 | unwind_op = project_op.children[0] 25 | assert(unwind_op.name == 'Unwind') 26 | assert(len(unwind_op.children) == 0) 27 | assert(unwind_op.profile_stats.records_produced == 4) 28 | 29 | # close the connection pool 30 | await pool.aclose() 31 | 32 | @pytest.mark.asyncio 33 | async def test_cartesian_product_profile(): 34 | pool = BlockingConnectionPool(max_connections=16, timeout=None, decode_responses=True) 35 | db = FalkorDB(connection_pool=pool) 36 | g = db.select_graph("async_profile") 37 | 38 | plan = await g.profile("MATCH (a), (b) RETURN *") 39 | 40 | results_op = plan.structured_plan 41 | assert(results_op.name == 'Results') 42 | assert(len(results_op.children) == 1) 43 | assert(results_op.profile_stats.records_produced == 0) 44 | 45 | project_op = results_op.children[0] 46 | assert(project_op.name == 'Project') 47 | assert(len(project_op.children) == 1) 48 | assert(project_op.profile_stats.records_produced == 0) 49 | 50 | cp_op = project_op.children[0] 51 | assert(cp_op.name == 'Cartesian Product') 52 | assert(len(cp_op.children) == 2) 53 | assert(cp_op.profile_stats.records_produced == 0) 54 | 55 | scan_a_op = cp_op.children[0] 56 | scan_b_op = cp_op.children[1] 57 | 58 | assert(scan_a_op.name == 'All Node Scan') 59 | assert(len(scan_a_op.children) == 0) 60 | assert(scan_a_op.profile_stats.records_produced == 0) 61 | 62 | assert(scan_b_op.name == 'All Node Scan') 63 | assert(len(scan_b_op.children) == 0) 64 | assert(scan_b_op.profile_stats.records_produced == 0) 65 | 66 | # close the connection pool 67 | await pool.aclose() 68 | -------------------------------------------------------------------------------- /tests/test_async_copy.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from falkordb.asyncio import FalkorDB 3 | from redis.asyncio import BlockingConnectionPool 4 | 5 | @pytest.mark.asyncio 6 | async def test_graph_copy(): 7 | # create a simple graph and clone it 8 | # make sure graphs are the same 9 | 10 | pool = BlockingConnectionPool(max_connections=16, timeout=None, decode_responses=True) 11 | db = FalkorDB(connection_pool=pool) 12 | src = db.select_graph("async_src") 13 | 14 | # create entities 15 | await src.query("CREATE (:A {v:1})-[:R {v:2}]->(:B {v:3})") 16 | 17 | # create index 18 | await src.create_edge_range_index("A", "v") 19 | await src.create_edge_range_index("R", "v") 20 | await src.create_node_fulltext_index("B", "v") 21 | 22 | # create constrain 23 | await src.create_node_unique_constraint("A", "v") 24 | await src.create_edge_unique_constraint("R", "v") 25 | 26 | # clone graph 27 | dest = await src.copy("async_dest") 28 | 29 | # validate src and dest are the same 30 | # validate entities 31 | q = "MATCH (a) RETURN a ORDER BY ID(a)" 32 | src_res = (await src.query(q)).result_set 33 | dest_res = (await dest.query(q)).result_set 34 | assert(src_res == dest_res) 35 | 36 | q = "MATCH ()-[e]->() RETURN e ORDER BY ID(e)" 37 | src_res = (await src.query(q)).result_set 38 | dest_res = (await dest.query(q)).result_set 39 | assert(src_res == dest_res) 40 | 41 | # validate schema 42 | src_res = (await src.call_procedure("DB.LABELS")).result_set 43 | dest_res = (await dest.call_procedure("DB.LABELS")).result_set 44 | assert(src_res == dest_res) 45 | 46 | src_res = (await src.call_procedure("DB.PROPERTYKEYS")).result_set 47 | dest_res = (await dest.call_procedure("DB.PROPERTYKEYS")).result_set 48 | assert(src_res == dest_res) 49 | 50 | src_res = (await src.call_procedure("DB.RELATIONSHIPTYPES")).result_set 51 | dest_res = (await dest.call_procedure("DB.RELATIONSHIPTYPES")).result_set 52 | assert(src_res == dest_res) 53 | 54 | # validate indices 55 | q = """CALL DB.INDEXES() 56 | YIELD label, properties, types, language, stopwords, entitytype, status 57 | RETURN * 58 | ORDER BY label, properties, types, language, stopwords, entitytype, status""" 59 | src_res = (await src.query(q)).result_set 60 | dest_res = (await dest.query(q)).result_set 61 | 62 | assert(src_res == dest_res) 63 | 64 | # validate constraints 65 | src_res = await src.list_constraints() 66 | dest_res = await dest.list_constraints() 67 | assert(src_res == dest_res) 68 | 69 | # close the connection pool 70 | await pool.aclose() 71 | -------------------------------------------------------------------------------- /tests/test_explain.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from falkordb import FalkorDB 3 | 4 | 5 | @pytest.fixture 6 | def client(request): 7 | db = FalkorDB(host='localhost', port=6379) 8 | return db 9 | 10 | 11 | def test_explain(client): 12 | db = client 13 | g = db.select_graph("explain") 14 | 15 | # run a single query to create the graph 16 | g.query("RETURN 1") 17 | 18 | plan = g.explain("UNWIND range(0, 3) AS x RETURN x") 19 | 20 | results_op = plan.structured_plan 21 | assert(results_op.name == 'Results') 22 | assert(len(results_op.children) == 1) 23 | 24 | project_op = results_op.children[0] 25 | assert(project_op.name == 'Project') 26 | assert(len(project_op.children) == 1) 27 | 28 | unwind_op = project_op.children[0] 29 | assert(unwind_op.name == 'Unwind') 30 | assert(len(unwind_op.children) == 0) 31 | 32 | def test_cartesian_product_explain(client): 33 | db = client 34 | g = db.select_graph("explain") 35 | plan = g.explain("MATCH (a), (b) RETURN *") 36 | 37 | results_op = plan.structured_plan 38 | assert(results_op.name == 'Results') 39 | assert(len(results_op.children) == 1) 40 | 41 | project_op = results_op.children[0] 42 | assert(project_op.name == 'Project') 43 | assert(len(project_op.children) == 1) 44 | 45 | cp_op = project_op.children[0] 46 | assert(cp_op.name == 'Cartesian Product') 47 | assert(len(cp_op.children) == 2) 48 | 49 | scan_a_op = cp_op.children[0] 50 | scan_b_op = cp_op.children[1] 51 | 52 | assert(scan_a_op.name == 'All Node Scan') 53 | assert(len(scan_a_op.children) == 0) 54 | 55 | assert(scan_b_op.name == 'All Node Scan') 56 | assert(len(scan_b_op.children) == 0) 57 | 58 | def test_merge(client): 59 | db = client 60 | g = db.select_graph("explain") 61 | 62 | try: 63 | g.create_node_range_index("person", "age") 64 | except: 65 | pass 66 | plan = g.explain("MERGE (p1:person {age: 40}) MERGE (p2:person {age: 41})") 67 | 68 | root = plan.structured_plan 69 | assert(root.name == 'Merge') 70 | assert(len(root.children) == 3) 71 | 72 | merge_op = root.children[0] 73 | assert(merge_op.name == 'Merge') 74 | assert(len(merge_op.children) == 2) 75 | 76 | index_scan_op = merge_op.children[0] 77 | assert(index_scan_op.name == 'Node By Index Scan') 78 | assert(len(index_scan_op.children) == 0) 79 | 80 | merge_create_op = merge_op.children[1] 81 | assert(merge_create_op.name == 'MergeCreate') 82 | assert(len(merge_create_op.children) == 0) 83 | 84 | index_scan_op = root.children[1] 85 | assert(index_scan_op.name == 'Node By Index Scan') 86 | assert(len(index_scan_op.children) == 1) 87 | 88 | arg_op = index_scan_op.children[0] 89 | assert(arg_op.name == 'Argument') 90 | assert(len(arg_op.children) == 0) 91 | 92 | merge_create_op = root.children[2] 93 | assert(merge_create_op.name == 'MergeCreate') 94 | assert(len(merge_create_op.children) == 1) 95 | 96 | arg_op = merge_create_op.children[0] 97 | assert(arg_op.name == 'Argument') 98 | assert(len(arg_op.children) == 0) 99 | -------------------------------------------------------------------------------- /tests/test_path.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from falkordb import Node, Edge, Path 3 | 4 | 5 | def test_init(): 6 | with pytest.raises(TypeError): 7 | Path(None, None) 8 | Path([], None) 9 | Path(None, []) 10 | 11 | assert isinstance(Path([], []), Path) 12 | 13 | 14 | def test_new_empty_path(): 15 | nodes = [] 16 | edges = [] 17 | path = Path(nodes, edges) 18 | assert isinstance(path, Path) 19 | assert path._nodes == [] 20 | assert path._edges == [] 21 | 22 | def test_wrong_flows(): 23 | node_1 = Node(node_id=1) 24 | node_2 = Node(node_id=2) 25 | node_3 = Node(node_id=3) 26 | 27 | edge_1 = Edge(node_1, None, node_2) 28 | edge_2 = Edge(node_1, None, node_3) 29 | 30 | nodes = [node_1, node_2, node_3] 31 | edges = [edge_1, edge_2] 32 | 33 | def test_nodes_and_edges(): 34 | node_1 = Node(node_id=1) 35 | node_2 = Node(node_id=2) 36 | edge_1 = Edge(node_1, None, node_2) 37 | 38 | nodes = [node_1, node_2] 39 | edges = [edge_1] 40 | 41 | p = Path(nodes, edges) 42 | assert nodes == p.nodes() 43 | assert node_1 == p.get_node(0) 44 | assert node_2 == p.get_node(1) 45 | assert node_1 == p.first_node() 46 | assert node_2 == p.last_node() 47 | assert 2 == p.node_count() 48 | 49 | assert edges == p.edges() 50 | assert 1 == p.edge_count() 51 | assert edge_1 == p.get_edge(0) 52 | 53 | assert p.get_node(-1) is None 54 | assert p.get_edge(49) is None 55 | 56 | path_str = str(p) 57 | assert path_str == "<(1)<-[]-(2)>" 58 | 59 | 60 | def test_compare(): 61 | node_1 = Node(node_id=1) 62 | node_2 = Node(node_id=2) 63 | edge_1 = Edge(node_1, None, node_2) 64 | nodes = [node_1, node_2] 65 | edges = [edge_1] 66 | 67 | assert Path([], []) == Path([], []) 68 | assert Path(nodes, edges) == Path(nodes, edges) 69 | assert Path(nodes, []) != Path([], []) 70 | assert Path([node_1], []) != Path([], []) 71 | assert Path([node_1], edges=[]) != Path([node_2], []) 72 | assert Path([node_1], [edge_1]) != Path( [node_1], []) 73 | assert Path([node_1], [edge_1]) != Path([node_2], [edge_1]) 74 | 75 | def test_str_with_none_edge_id(): 76 | """Test that Path.__str__() works when edge.id is None""" 77 | node_1 = Node(node_id=1) 78 | node_2 = Node(node_id=2) 79 | edge_1 = Edge(node_1, None, node_2) 80 | 81 | nodes = [node_1, node_2] 82 | edges = [edge_1] 83 | 84 | p = Path(nodes, edges) 85 | # Should not raise an exception 86 | path_str = str(p) 87 | assert isinstance(path_str, str) 88 | # The edge should be represented with empty brackets since id is None 89 | assert ('<-[]-' in path_str) or ('-[]->' in path_str) 90 | 91 | def test_str_with_edge_id(): 92 | """Test that Path.__str__() works when edge.id is provided""" 93 | node_1 = Node(node_id=1) 94 | node_2 = Node(node_id=2) 95 | edge_1 = Edge(node_1, None, node_2, edge_id=10) 96 | 97 | nodes = [node_1, node_2] 98 | edges = [edge_1] 99 | 100 | p = Path(nodes, edges) 101 | path_str = str(p) 102 | assert isinstance(path_str, str) 103 | # The edge should be represented with the ID 104 | assert "10" in path_str 105 | -------------------------------------------------------------------------------- /falkordb/node.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Union 2 | from .helpers import quote_string 3 | 4 | class Node: 5 | """ 6 | A graph node. 7 | """ 8 | 9 | def __init__(self, node_id: Optional[int] = None, 10 | alias: Optional[str] = '', 11 | labels: Optional[Union[str, List[str]]] = None, 12 | properties=None): 13 | """ 14 | Create a new node. 15 | 16 | Args: 17 | node_id: The ID of the node. 18 | alias: An alias for the node (default is empty string). 19 | labels: The label or list of labels for the node. 20 | properties: The properties of the node. 21 | 22 | Returns: 23 | None 24 | """ 25 | self.id = node_id 26 | self.alias = alias 27 | self.labels = None 28 | 29 | if isinstance(labels, list): 30 | self.labels = [l for l in labels if isinstance(l, str) and l != ""] 31 | elif isinstance(labels, str) and labels != "": 32 | self.labels = [labels] 33 | 34 | self.properties = properties or {} 35 | 36 | def to_string(self) -> str: 37 | """ 38 | Get a string representation of the node's properties. 39 | 40 | Returns: 41 | str: A string representation of the node's properties. 42 | """ 43 | res = "" 44 | if self.properties: 45 | props = ",".join( 46 | key + ":" + str(quote_string(val)) 47 | for key, val in sorted(self.properties.items()) 48 | ) 49 | res += "{" + props + "}" 50 | 51 | return res 52 | 53 | def __str__(self) -> str: 54 | """ 55 | Get a string representation of the node. 56 | 57 | Returns: 58 | str: A string representation of the node. 59 | """ 60 | res = "(" 61 | if self.alias: 62 | res += self.alias 63 | if self.labels: 64 | res += ":" + ":".join(self.labels) 65 | if self.properties: 66 | props = ",".join( 67 | key + ":" + str(quote_string(val)) 68 | for key, val in sorted(self.properties.items()) 69 | ) 70 | res += "{" + props + "}" 71 | res += ")" 72 | 73 | return res 74 | 75 | def __eq__(self, rhs) -> bool: 76 | """ 77 | Check if two nodes are equal. 78 | 79 | Args: 80 | rhs: The node to compare. 81 | 82 | Returns: 83 | bool: True if the nodes are equal, False otherwise. 84 | """ 85 | # Type checking 86 | if not isinstance(rhs, Node): 87 | return False 88 | 89 | # Quick positive check, if both IDs are set 90 | if self.id is not None and rhs.id is not None and self.id != rhs.id: 91 | return False 92 | 93 | # Labels should match. 94 | if self.labels != rhs.labels: 95 | return False 96 | 97 | # Quick check for the number of properties. 98 | if len(self.properties) != len(rhs.properties): 99 | return False 100 | 101 | # Compare properties. 102 | if self.properties != rhs.properties: 103 | return False 104 | 105 | return True 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![license](https://img.shields.io/github/license/falkordb/falkordb-py.svg)](https://github.com/falkordb/falkordb-py) 2 | [![Release](https://img.shields.io/github/release/falkordb/falkordb-py.svg)](https://github.com/falkordb/falkordb-py/releases/latest) 3 | [![PyPI version](https://badge.fury.io/py/falkordb.svg)](https://badge.fury.io/py/falkordb) 4 | [![Codecov](https://codecov.io/gh/falkordb/falkordb-py/branch/main/graph/badge.svg)](https://codecov.io/gh/falkordb/falkordb-py) 5 | [![Forum](https://img.shields.io/badge/Forum-falkordb-blue)](https://github.com/orgs/FalkorDB/discussions) 6 | [![Discord](https://img.shields.io/discord/1146782921294884966?style=flat-square)](https://discord.gg/ErBEqN9E) 7 | 8 | # falkordb-py 9 | 10 | [![Try Free](https://img.shields.io/badge/Try%20Free-FalkorDB%20Cloud-FF8101?labelColor=FDE900&style=for-the-badge&link=https://app.falkordb.cloud)](https://app.falkordb.cloud) 11 | 12 | FalkorDB Python client 13 | 14 | see [docs](http://falkordb-py.readthedocs.io/) 15 | 16 | ## Installation 17 | ```sh 18 | pip install FalkorDB 19 | ``` 20 | 21 | ## Usage 22 | 23 | ### Run FalkorDB instance 24 | Docker: 25 | ```sh 26 | docker run --rm -p 6379:6379 falkordb/falkordb 27 | ``` 28 | Or use [FalkorDB Cloud](https://app.falkordb.cloud) 29 | 30 | ### Synchronous Example 31 | 32 | ```python 33 | from falkordb import FalkorDB 34 | 35 | # Connect to FalkorDB 36 | db = FalkorDB(host='localhost', port=6379) 37 | 38 | # Select the social graph 39 | g = db.select_graph('social') 40 | 41 | # Create 100 nodes and return a handful 42 | nodes = g.query('UNWIND range(0, 100) AS i CREATE (n {v:1}) RETURN n LIMIT 10').result_set 43 | for n in nodes: 44 | print(n) 45 | 46 | # Read-only query the graph for the first 10 nodes 47 | nodes = g.ro_query('MATCH (n) RETURN n LIMIT 10').result_set 48 | 49 | # Copy the Graph 50 | copy_graph = g.copy('social_copy') 51 | 52 | # Delete the Graph 53 | g.delete() 54 | ``` 55 | 56 | ### Asynchronous Example 57 | 58 | ```python 59 | import asyncio 60 | from falkordb.asyncio import FalkorDB 61 | from redis.asyncio import BlockingConnectionPool 62 | 63 | async def main(): 64 | 65 | # Connect to FalkorDB 66 | pool = BlockingConnectionPool(max_connections=16, timeout=None, decode_responses=True) 67 | db = FalkorDB(connection_pool=pool) 68 | 69 | # Select the social graph 70 | g = db.select_graph('social') 71 | 72 | # Execute query asynchronously 73 | result = await g.query('UNWIND range(0, 100) AS i CREATE (n {v:1}) RETURN n LIMIT 10') 74 | 75 | # Process results 76 | for n in result.result_set: 77 | print(n) 78 | 79 | # Run multiple queries concurrently 80 | tasks = [ 81 | g.query('MATCH (n) WHERE n.v = 1 RETURN count(n) AS count'), 82 | g.query('CREATE (p:Person {name: "Alice"}) RETURN p'), 83 | g.query('CREATE (p:Person {name: "Bob"}) RETURN p') 84 | ] 85 | 86 | results = await asyncio.gather(*tasks) 87 | 88 | # Process concurrent results 89 | print(f"Node count: {results[0].result_set[0][0]}") 90 | print(f"Created Alice: {results[1].result_set[0][0]}") 91 | print(f"Created Bob: {results[2].result_set[0][0]}") 92 | 93 | # Close the connection when done 94 | await pool.aclose() 95 | 96 | # Run the async example 97 | if __name__ == "__main__": 98 | asyncio.run(main()) 99 | ``` 100 | -------------------------------------------------------------------------------- /tests/test_async_explain.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from falkordb.asyncio import FalkorDB 3 | from redis.asyncio import BlockingConnectionPool 4 | 5 | 6 | @pytest.mark.asyncio 7 | async def test_explain(): 8 | pool = BlockingConnectionPool(max_connections=16, timeout=None, decode_responses=True) 9 | db = FalkorDB(connection_pool=pool) 10 | g = db.select_graph("async_explain") 11 | 12 | # run a single query to create the graph 13 | await g.query("RETURN 1") 14 | 15 | plan = await g.explain("UNWIND range(0, 3) AS x RETURN x") 16 | 17 | results_op = plan.structured_plan 18 | assert(results_op.name == 'Results') 19 | assert(len(results_op.children) == 1) 20 | 21 | project_op = results_op.children[0] 22 | assert(project_op.name == 'Project') 23 | assert(len(project_op.children) == 1) 24 | 25 | unwind_op = project_op.children[0] 26 | assert(unwind_op.name == 'Unwind') 27 | assert(len(unwind_op.children) == 0) 28 | 29 | # close the connection pool 30 | await pool.aclose() 31 | 32 | @pytest.mark.asyncio 33 | async def test_cartesian_product_explain(): 34 | pool = BlockingConnectionPool(max_connections=16, timeout=None, decode_responses=True) 35 | db = FalkorDB(connection_pool=pool) 36 | g = db.select_graph("async_explain") 37 | plan = await g.explain("MATCH (a), (b) RETURN *") 38 | 39 | results_op = plan.structured_plan 40 | assert(results_op.name == 'Results') 41 | assert(len(results_op.children) == 1) 42 | 43 | project_op = results_op.children[0] 44 | assert(project_op.name == 'Project') 45 | assert(len(project_op.children) == 1) 46 | 47 | cp_op = project_op.children[0] 48 | assert(cp_op.name == 'Cartesian Product') 49 | assert(len(cp_op.children) == 2) 50 | 51 | scan_a_op = cp_op.children[0] 52 | scan_b_op = cp_op.children[1] 53 | 54 | assert(scan_a_op.name == 'All Node Scan') 55 | assert(len(scan_a_op.children) == 0) 56 | 57 | assert(scan_b_op.name == 'All Node Scan') 58 | assert(len(scan_b_op.children) == 0) 59 | 60 | # close the connection pool 61 | await pool.aclose() 62 | 63 | @pytest.mark.asyncio 64 | async def test_merge(): 65 | pool = BlockingConnectionPool(max_connections=16, timeout=None, decode_responses=True) 66 | db = FalkorDB(connection_pool=pool) 67 | g = db.select_graph("async_explain") 68 | 69 | try: 70 | await g.create_node_range_index("person", "age") 71 | except: 72 | pass 73 | plan = await g.explain("MERGE (p1:person {age: 40}) MERGE (p2:person {age: 41})") 74 | 75 | root = plan.structured_plan 76 | assert(root.name == 'Merge') 77 | assert(len(root.children) == 3) 78 | 79 | merge_op = root.children[0] 80 | assert(merge_op.name == 'Merge') 81 | assert(len(merge_op.children) == 2) 82 | 83 | index_scan_op = merge_op.children[0] 84 | assert(index_scan_op.name == 'Node By Index Scan') 85 | assert(len(index_scan_op.children) == 0) 86 | 87 | merge_create_op = merge_op.children[1] 88 | assert(merge_create_op.name == 'MergeCreate') 89 | assert(len(merge_create_op.children) == 0) 90 | 91 | index_scan_op = root.children[1] 92 | assert(index_scan_op.name == 'Node By Index Scan') 93 | assert(len(index_scan_op.children) == 1) 94 | 95 | arg_op = index_scan_op.children[0] 96 | assert(arg_op.name == 'Argument') 97 | assert(len(arg_op.children) == 0) 98 | 99 | merge_create_op = root.children[2] 100 | assert(merge_create_op.name == 'MergeCreate') 101 | assert(len(merge_create_op.children) == 1) 102 | 103 | arg_op = merge_create_op.children[0] 104 | assert(arg_op.name == 'Argument') 105 | assert(len(arg_op.children) == 0) 106 | 107 | # close the connection pool 108 | await pool.aclose() 109 | -------------------------------------------------------------------------------- /falkordb/graph_schema.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from .exceptions import SchemaVersionMismatchException 3 | 4 | # procedures 5 | DB_LABELS = "DB.LABELS" 6 | DB_PROPERTYKEYS = "DB.PROPERTYKEYS" 7 | DB_RELATIONSHIPTYPES = "DB.RELATIONSHIPTYPES" 8 | 9 | 10 | class GraphSchema(): 11 | """ 12 | The graph schema. 13 | Maintains the labels, properties and relationships of the graph. 14 | """ 15 | 16 | def __init__(self, graph: 'Graph'): 17 | """ 18 | Initialize the graph schema. 19 | 20 | Args: 21 | graph (Graph): The graph. 22 | 23 | Returns: 24 | GraphSchema: The graph schema. 25 | """ 26 | 27 | self.graph = graph 28 | self.clear() 29 | 30 | def clear(self): 31 | """ 32 | Clear the graph schema. 33 | 34 | Returns: 35 | None 36 | 37 | """ 38 | 39 | self.version = 0 40 | self.labels = [] 41 | self.properties = [] 42 | self.relationships = [] 43 | 44 | def refresh_labels(self) -> None: 45 | """ 46 | Refresh labels. 47 | 48 | Returns: 49 | None 50 | 51 | """ 52 | 53 | result_set = self.graph.call_procedure(DB_LABELS).result_set 54 | self.labels = [l[0] for l in result_set] 55 | 56 | def refresh_relations(self) -> None: 57 | """ 58 | Refresh relationship types. 59 | 60 | Returns: 61 | None 62 | 63 | """ 64 | 65 | result_set = self.graph.call_procedure(DB_RELATIONSHIPTYPES).result_set 66 | self.relationships = [r[0] for r in result_set] 67 | 68 | def refresh_properties(self) -> None: 69 | """ 70 | Refresh property keys. 71 | 72 | Returns: 73 | None 74 | 75 | """ 76 | 77 | result_set = self.graph.call_procedure(DB_PROPERTYKEYS).result_set 78 | self.properties = [p[0] for p in result_set] 79 | 80 | def refresh(self, version: int) -> None: 81 | """ 82 | Refresh the graph schema. 83 | 84 | Args: 85 | version (int): The version of the graph schema. 86 | 87 | Returns: 88 | None 89 | 90 | """ 91 | 92 | self.clear() 93 | self.version = version 94 | self.refresh_labels() 95 | self.refresh_relations() 96 | self.refresh_properties() 97 | 98 | def get_label(self, idx: int) -> str: 99 | """ 100 | Returns a label by its index. 101 | 102 | Args: 103 | idx (int): The index of the label. 104 | 105 | Returns: 106 | str: The label. 107 | 108 | """ 109 | 110 | try: 111 | l = self.labels[idx] 112 | except IndexError: 113 | # refresh labels 114 | self.refresh_labels() 115 | l = self.labels[idx] 116 | return l 117 | 118 | def get_relation(self, idx: int) -> str: 119 | """ 120 | Returns a relationship type by its index. 121 | 122 | Args: 123 | idx (int): The index of the relation. 124 | 125 | Returns: 126 | str: The relationship type. 127 | 128 | """ 129 | 130 | try: 131 | r = self.relationships[idx] 132 | except IndexError: 133 | # refresh relationship types 134 | self.refresh_relations() 135 | r = self.relationships[idx] 136 | return r 137 | 138 | def get_property(self, idx: int) -> str: 139 | """ 140 | Returns a property by its index. 141 | 142 | Args: 143 | idx (int): The index of the property. 144 | 145 | Returns: 146 | str: The property. 147 | 148 | """ 149 | 150 | try: 151 | p = self.properties[idx] 152 | except IndexError: 153 | # refresh properties 154 | self.refresh_properties() 155 | p = self.properties[idx] 156 | return p 157 | -------------------------------------------------------------------------------- /falkordb/edge.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from .node import Node 3 | from .helpers import quote_string 4 | 5 | class Edge: 6 | """ 7 | An edge connecting two nodes. 8 | """ 9 | 10 | def __init__(self, src_node: Node, relation: str, dest_node: Node, 11 | edge_id: Optional[int] = None, alias: Optional[str] = '', 12 | properties=None): 13 | """ 14 | Create a new edge. 15 | 16 | Args: 17 | src_node: The source node of the edge. 18 | relation: The relationship type of the edge. 19 | dest_node: The destination node of the edge. 20 | edge_id: The ID of the edge. 21 | alias: An alias for the edge (default is empty string). 22 | properties: The properties of the edge. 23 | 24 | Raises: 25 | AssertionError: If either src_node or dest_node is not provided. 26 | 27 | Returns: 28 | None 29 | """ 30 | if src_node is None or dest_node is None: 31 | raise AssertionError("Both src_node & dest_node must be provided") 32 | 33 | self.id = edge_id 34 | self.alias = alias 35 | self.src_node = src_node 36 | self.dest_node = dest_node 37 | self.relation = relation 38 | self.properties = properties or {} 39 | 40 | def to_string(self) -> str: 41 | """ 42 | Get a string representation of the edge's properties. 43 | 44 | Returns: 45 | str: A string representation of the edge's properties. 46 | """ 47 | res = "" 48 | if self.properties: 49 | props = ",".join( 50 | key + ":" + str(quote_string(val)) 51 | for key, val in sorted(self.properties.items()) 52 | ) 53 | res += "{" + props + "}" 54 | 55 | return res 56 | 57 | def __str__(self) -> str: 58 | """ 59 | Get a string representation of the edge. 60 | 61 | Returns: 62 | str: A string representation of the edge. 63 | """ 64 | # Source node 65 | if isinstance(self.src_node, Node): 66 | res = f"({self.src_node.alias})" 67 | else: 68 | res = "()" 69 | 70 | # Edge 71 | res += f"-[{self.alias}" 72 | if self.relation: 73 | res += ":" + self.relation 74 | if self.properties: 75 | props = ",".join( 76 | key + ":" + str(quote_string(val)) 77 | for key, val in sorted(self.properties.items()) 78 | ) 79 | res += f"{{{props}}}" 80 | res += "]->" 81 | 82 | # Dest node 83 | if isinstance(self.dest_node, Node): 84 | res += f"({self.dest_node.alias})" 85 | else: 86 | res += "()" 87 | 88 | return res 89 | 90 | def __eq__(self, rhs) -> bool: 91 | """ 92 | Check if two edges are equal. 93 | 94 | Args: 95 | rhs: The edge to compare. 96 | 97 | Returns: 98 | bool: True if the edges are equal, False otherwise. 99 | """ 100 | # Type checking 101 | if not isinstance(rhs, Edge): 102 | return False 103 | 104 | # Quick positive check, if both IDs are set 105 | if self.id is not None and rhs.id is not None and self.id == rhs.id: 106 | return True 107 | 108 | # Source and destination nodes should match 109 | if self.src_node != rhs.src_node: 110 | return False 111 | 112 | if self.dest_node != rhs.dest_node: 113 | return False 114 | 115 | # Relation should match 116 | if self.relation != rhs.relation: 117 | return False 118 | 119 | # Quick check for the number of properties 120 | if len(self.properties) != len(rhs.properties): 121 | return False 122 | 123 | # Compare properties 124 | if self.properties != rhs.properties: 125 | return False 126 | 127 | return True 128 | -------------------------------------------------------------------------------- /falkordb/asyncio/graph_schema.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from falkordb.exceptions import SchemaVersionMismatchException 3 | 4 | # procedures 5 | DB_LABELS = "DB.LABELS" 6 | DB_PROPERTYKEYS = "DB.PROPERTYKEYS" 7 | DB_RELATIONSHIPTYPES = "DB.RELATIONSHIPTYPES" 8 | 9 | 10 | class GraphSchema(): 11 | """ 12 | The graph schema. 13 | Maintains the labels, properties and relationships of the graph. 14 | """ 15 | 16 | def __init__(self, graph: 'Graph'): 17 | """ 18 | Initialize the graph schema. 19 | 20 | Args: 21 | graph (Graph): The graph. 22 | 23 | Returns: 24 | GraphSchema: The graph schema. 25 | """ 26 | 27 | self.graph = graph 28 | self.clear() 29 | 30 | def clear(self): 31 | """ 32 | Clear the graph schema. 33 | 34 | Returns: 35 | None 36 | 37 | """ 38 | 39 | self.version = 0 40 | self.labels = [] 41 | self.properties = [] 42 | self.relationships = [] 43 | 44 | async def refresh_labels(self) -> None: 45 | """ 46 | Refresh labels. 47 | 48 | Returns: 49 | None 50 | 51 | """ 52 | 53 | result_set = (await self.graph.call_procedure(DB_LABELS)).result_set 54 | self.labels = [l[0] for l in result_set] 55 | 56 | async def refresh_relations(self) -> None: 57 | """ 58 | Refresh relationship types. 59 | 60 | Returns: 61 | None 62 | 63 | """ 64 | 65 | result_set = (await self.graph.call_procedure(DB_RELATIONSHIPTYPES)).result_set 66 | self.relationships = [r[0] for r in result_set] 67 | 68 | async def refresh_properties(self) -> None: 69 | """ 70 | Refresh property keys. 71 | 72 | Returns: 73 | None 74 | 75 | """ 76 | 77 | result_set = (await self.graph.call_procedure(DB_PROPERTYKEYS)).result_set 78 | self.properties = [p[0] for p in result_set] 79 | 80 | async def refresh(self, version: int) -> None: 81 | """ 82 | Refresh the graph schema. 83 | 84 | Args: 85 | version (int): The version of the graph schema. 86 | 87 | Returns: 88 | None 89 | 90 | """ 91 | 92 | self.clear() 93 | self.version = version 94 | await self.refresh_labels() 95 | await self.refresh_relations() 96 | await self.refresh_properties() 97 | 98 | async def get_label(self, idx: int) -> str: 99 | """ 100 | Returns a label by its index. 101 | 102 | Args: 103 | idx (int): The index of the label. 104 | 105 | Returns: 106 | str: The label. 107 | 108 | """ 109 | 110 | try: 111 | l = self.labels[idx] 112 | except IndexError: 113 | # refresh labels 114 | await self.refresh_labels() 115 | l = self.labels[idx] 116 | return l 117 | 118 | async def get_relation(self, idx: int) -> str: 119 | """ 120 | Returns a relationship type by its index. 121 | 122 | Args: 123 | idx (int): The index of the relation. 124 | 125 | Returns: 126 | str: The relationship type. 127 | 128 | """ 129 | 130 | try: 131 | r = self.relationships[idx] 132 | except IndexError: 133 | # refresh relationship types 134 | await self.refresh_relations() 135 | r = self.relationships[idx] 136 | return r 137 | 138 | async def get_property(self, idx: int) -> str: 139 | """ 140 | Returns a property by its index. 141 | 142 | Args: 143 | idx (int): The index of the property. 144 | 145 | Returns: 146 | str: The property. 147 | 148 | """ 149 | 150 | try: 151 | p = self.properties[idx] 152 | except IndexError: 153 | # refresh properties 154 | await self.refresh_properties() 155 | p = self.properties[idx] 156 | return p 157 | -------------------------------------------------------------------------------- /falkordb/path.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from .edge import Edge 3 | from .node import Node 4 | 5 | 6 | class Path: 7 | """ 8 | Path Class for representing a path in a graph. 9 | 10 | This class defines a path consisting of nodes and edges. It provides methods for managing and manipulating the path. 11 | 12 | Example: 13 | node1 = Node() 14 | node2 = Node() 15 | edge1 = Edge(node1, "R", node2) 16 | 17 | path = Path.new_empty_path() 18 | path.add_node(node1).add_edge(edge1).add_node(node2) 19 | print(path) 20 | # Output: <(node1)-(edge1)->(node2)> 21 | """ 22 | def __init__(self, nodes: List[Node], edges: List[Edge]): 23 | if not (isinstance(nodes, list) and isinstance(edges, list)): 24 | raise TypeError("nodes and edges must be list") 25 | 26 | self._nodes = nodes 27 | self._edges = edges 28 | self.append_type = Node 29 | 30 | def nodes(self) -> List[Node]: 31 | """ 32 | Returns the list of nodes in the path. 33 | 34 | Returns: 35 | list: List of nodes in the path. 36 | """ 37 | return self._nodes 38 | 39 | def edges(self) -> List[Edge]: 40 | """ 41 | Returns the list of edges in the path. 42 | 43 | Returns: 44 | list: List of edges in the path. 45 | """ 46 | return self._edges 47 | 48 | def get_node(self, index) -> Node: 49 | """ 50 | Returns the node at the specified index in the path. 51 | 52 | Args: 53 | index (int): Index of the node. 54 | 55 | Returns: 56 | Node: The node at the specified index. 57 | """ 58 | if 0 <= index < self.node_count(): 59 | return self._nodes[index] 60 | 61 | return None 62 | 63 | def get_edge(self, index) -> Edge: 64 | """ 65 | Returns the edge at the specified index in the path. 66 | 67 | Args: 68 | index (int): Index of the edge. 69 | 70 | Returns: 71 | Edge: The edge at the specified index. 72 | """ 73 | if 0 <= index < self.edge_count(): 74 | return self._edges[index] 75 | 76 | return None 77 | 78 | def first_node(self) -> Node: 79 | """ 80 | Returns the first node in the path. 81 | 82 | Returns: 83 | Node: The first node in the path. 84 | """ 85 | return self._nodes[0] if self.node_count() > 0 else None 86 | 87 | def last_node(self) -> Node: 88 | """ 89 | Returns the last node in the path. 90 | 91 | Returns: 92 | Node: The last node in the path. 93 | """ 94 | return self._nodes[-1] if self.node_count() > 0 else None 95 | 96 | def edge_count(self) -> int: 97 | """ 98 | Returns the number of edges in the path. 99 | 100 | Returns: 101 | int: Number of edges in the path. 102 | """ 103 | return len(self._edges) 104 | 105 | def node_count(self) -> int: 106 | """ 107 | Returns the number of nodes in the path. 108 | 109 | Returns: 110 | int: Number of nodes in the path. 111 | """ 112 | return len(self._nodes) 113 | 114 | def __eq__(self, other) -> bool: 115 | """ 116 | Compares two Path instances for equality based on their nodes and edges. 117 | 118 | Args: 119 | other (Path): Another Path instance for comparison. 120 | 121 | Returns: 122 | bool: True if the paths are equal, False otherwise. 123 | """ 124 | # Type checking 125 | if not isinstance(other, Path): 126 | return False 127 | 128 | return self.nodes() == other.nodes() and self.edges() == other.edges() 129 | 130 | def __str__(self) -> str: 131 | """ 132 | Returns a string representation of the path, including nodes and edges. 133 | 134 | Returns: 135 | str: String representation of the path. 136 | """ 137 | res = "<" 138 | edge_count = self.edge_count() 139 | for i in range(0, edge_count): 140 | node_id = self.get_node(i).id 141 | res += "(" + str(node_id) + ")" 142 | edge = self.get_edge(i) 143 | edge_id_str = str(int(edge.id)) if edge.id is not None else "" 144 | res += ( 145 | "-[" + edge_id_str + "]->" 146 | if edge.src_node == node_id 147 | else "<-[" + edge_id_str + "]-" 148 | ) 149 | node_id = self.get_node(edge_count).id 150 | res += "(" + str(node_id) + ")" 151 | res += ">" 152 | return res 153 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, religion, or sexual identity 11 | and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the 27 | overall community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or 32 | advances of any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email 36 | address, without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official email address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at info@falkordb.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available 126 | at [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /tests/test_indices.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from redis import ResponseError 3 | from falkordb import FalkorDB 4 | from collections import OrderedDict 5 | 6 | class Index(): 7 | def __init__(self, raw_response): 8 | self.label = raw_response[0] 9 | self.properties = raw_response[1] 10 | self.types = raw_response[2] 11 | self.entity_type = raw_response[6] 12 | 13 | @pytest.fixture 14 | def client(request): 15 | db = FalkorDB(host='localhost', port=6379) 16 | db.flushdb() 17 | return db.select_graph("indices") 18 | 19 | def test_node_index_creation(client): 20 | graph = client 21 | lbl = "N" 22 | 23 | # create node indices 24 | 25 | # create node range index 26 | res = graph.create_node_range_index(lbl, 'x') 27 | assert(res.indices_created == 1) 28 | 29 | index = Index(graph.list_indices().result_set[0]) 30 | assert(index.label == lbl) 31 | assert(index.properties == ['x']) 32 | assert(index.types['x'] == ['RANGE']) 33 | assert(index.entity_type == 'NODE') 34 | 35 | # create node range index over multiple properties 36 | res = graph.create_node_range_index(lbl, 'y', 'z') 37 | assert(res.indices_created == 2) 38 | 39 | index = Index(graph.list_indices().result_set[0]) 40 | assert(index.label == lbl) 41 | assert(index.properties == ['x', 'y', 'z']) 42 | assert(index.types['x'] == ['RANGE']) 43 | assert(index.types['y'] == ['RANGE']) 44 | assert(index.types['z'] == ['RANGE']) 45 | assert(index.entity_type == 'NODE') 46 | 47 | # try to create an existing index 48 | with pytest.raises(ResponseError): 49 | res = graph.create_node_range_index(lbl, 'z', 'x') 50 | 51 | # create node full-text index 52 | res = graph.create_node_fulltext_index(lbl, 'name') 53 | assert(res.indices_created == 1) 54 | 55 | index = Index(graph.list_indices().result_set[0]) 56 | assert(index.label == lbl) 57 | assert(index.properties == ['x', 'y', 'z', 'name']) 58 | assert(index.types['x'] == ['RANGE']) 59 | assert(index.types['y'] == ['RANGE']) 60 | assert(index.types['z'] == ['RANGE']) 61 | assert(index.types['name'] == ['FULLTEXT']) 62 | assert(index.entity_type == 'NODE') 63 | 64 | # create node vector index 65 | res = graph.create_node_vector_index(lbl, 'desc', dim=32, similarity_function="euclidean") 66 | assert(res.indices_created == 1) 67 | 68 | index = Index(graph.list_indices().result_set[0]) 69 | assert(index.label == lbl) 70 | assert(index.properties == ['x', 'y', 'z', 'name', 'desc']) 71 | assert(index.types['x'] == ['RANGE']) 72 | assert(index.types['y'] == ['RANGE']) 73 | assert(index.types['z'] == ['RANGE']) 74 | assert(index.types['name'] == ['FULLTEXT']) 75 | assert(index.types['desc'] == ['VECTOR']) 76 | assert(index.entity_type == 'NODE') 77 | 78 | # create a multi-type property 79 | res = graph.create_node_fulltext_index(lbl, 'x') 80 | assert(res.indices_created == 1) 81 | 82 | index = Index(graph.list_indices().result_set[0]) 83 | assert(index.label == lbl) 84 | assert(index.properties == ['x', 'y', 'z', 'name', 'desc']) 85 | assert(index.types['x'] == ['RANGE', 'FULLTEXT']) 86 | assert(index.types['y'] == ['RANGE']) 87 | assert(index.types['z'] == ['RANGE']) 88 | assert(index.types['name'] == ['FULLTEXT']) 89 | assert(index.types['desc'] == ['VECTOR']) 90 | assert(index.entity_type == 'NODE') 91 | 92 | def test_edge_index_creation(client): 93 | graph = client 94 | rel = "R" 95 | 96 | # create edge indices 97 | 98 | # create edge range index 99 | res = graph.create_edge_range_index(rel, 'x') 100 | assert(res.indices_created == 1) 101 | 102 | index = Index(graph.list_indices().result_set[0]) 103 | assert(index.label ==rel) 104 | assert(index.properties == ['x']) 105 | assert(index.types['x'] == ['RANGE']) 106 | assert(index.entity_type == 'RELATIONSHIP') 107 | 108 | # create edge range index over multiple properties 109 | res = graph.create_edge_range_index(rel, 'y', 'z') 110 | assert(res.indices_created == 2) 111 | 112 | index = Index(graph.list_indices().result_set[0]) 113 | assert(index.label ==rel) 114 | assert(index.properties == ['x', 'y', 'z']) 115 | assert(index.types['x'] == ['RANGE']) 116 | assert(index.types['y'] == ['RANGE']) 117 | assert(index.types['z'] == ['RANGE']) 118 | assert(index.entity_type == 'RELATIONSHIP') 119 | 120 | # try to create an existing index 121 | with pytest.raises(ResponseError): 122 | res = graph.create_edge_range_index(rel, 'z', 'x') 123 | 124 | # create edge full-text index 125 | res = graph.create_edge_fulltext_index(rel, 'name') 126 | assert(res.indices_created == 1) 127 | 128 | index = Index(graph.list_indices().result_set[0]) 129 | assert(index.label ==rel) 130 | assert(index.properties == ['x', 'y', 'z', 'name']) 131 | assert(index.types['x'] == ['RANGE']) 132 | assert(index.types['y'] == ['RANGE']) 133 | assert(index.types['z'] == ['RANGE']) 134 | assert(index.types['name'] == ['FULLTEXT']) 135 | assert(index.entity_type == 'RELATIONSHIP') 136 | 137 | # create edge vector index 138 | res = graph.create_edge_vector_index(rel, 'desc', dim=32, similarity_function="euclidean") 139 | assert(res.indices_created == 1) 140 | 141 | index = Index(graph.list_indices().result_set[0]) 142 | assert(index.label ==rel) 143 | assert(index.properties == ['x', 'y', 'z', 'name', 'desc']) 144 | assert(index.types['x'] == ['RANGE']) 145 | assert(index.types['y'] == ['RANGE']) 146 | assert(index.types['z'] == ['RANGE']) 147 | assert(index.types['name'] == ['FULLTEXT']) 148 | assert(index.types['desc'] == ['VECTOR']) 149 | assert(index.entity_type == 'RELATIONSHIP') 150 | 151 | # create a multi-type property 152 | res = graph.create_edge_fulltext_index(rel, 'x') 153 | assert(res.indices_created == 1) 154 | 155 | index = Index(graph.list_indices().result_set[0]) 156 | assert(index.label ==rel) 157 | assert(index.properties == ['x', 'y', 'z', 'name', 'desc']) 158 | assert(index.types['x'] == ['RANGE', 'FULLTEXT']) 159 | assert(index.types['y'] == ['RANGE']) 160 | assert(index.types['z'] == ['RANGE']) 161 | assert(index.types['name'] == ['FULLTEXT']) 162 | assert(index.types['desc'] == ['VECTOR']) 163 | assert(index.entity_type == 'RELATIONSHIP') 164 | 165 | def test_node_index_drop(client): 166 | graph = client 167 | 168 | # create an index and delete it 169 | lbl = 'N' 170 | attr = 'x' 171 | 172 | # create node range index 173 | res = graph.create_node_range_index(lbl, attr) 174 | assert(res.indices_created == 1) 175 | 176 | # list indices 177 | res = graph.list_indices() 178 | assert(len(res.result_set) == 1) 179 | 180 | # drop range index 181 | res = graph.drop_node_range_index(lbl, attr) 182 | assert(res.indices_deleted == 1) 183 | 184 | # list indices 185 | res = graph.list_indices() 186 | assert(len(res.result_set) == 0) 187 | 188 | #--------------------------------------------------------------------------- 189 | 190 | # create node fulltext index 191 | res = graph.create_node_fulltext_index(lbl, attr) 192 | assert(res.indices_created == 1) 193 | 194 | # list indices 195 | res = graph.list_indices() 196 | assert(len(res.result_set) == 1) 197 | 198 | # drop fulltext index 199 | res = graph.drop_node_fulltext_index(lbl, attr) 200 | assert(res.indices_deleted == 1) 201 | 202 | # list indices 203 | res = graph.list_indices() 204 | assert(len(res.result_set) == 0) 205 | 206 | #--------------------------------------------------------------------------- 207 | 208 | # create node vector index 209 | res = graph.create_node_vector_index(lbl, attr) 210 | assert(res.indices_created == 1) 211 | 212 | # list indices 213 | res = graph.list_indices() 214 | assert(len(res.result_set) == 1) 215 | 216 | # drop vector index 217 | res = graph.drop_node_vector_index(lbl, attr) 218 | assert(res.indices_deleted == 1) 219 | 220 | # list indices 221 | res = graph.list_indices() 222 | assert(len(res.result_set) == 0) 223 | 224 | def test_edge_index_drop(client): 225 | graph = client 226 | 227 | # create an index and delete it 228 | rel = 'R' 229 | attr = 'x' 230 | 231 | # create edge range index 232 | res = graph.create_edge_range_index(rel, attr) 233 | assert(res.indices_created == 1) 234 | 235 | # list indices 236 | res = graph.list_indices() 237 | assert(len(res.result_set) == 1) 238 | 239 | # drop range index 240 | res = graph.drop_edge_range_index(rel, attr) 241 | assert(res.indices_deleted == 1) 242 | 243 | # list indices 244 | res = graph.list_indices() 245 | assert(len(res.result_set) == 0) 246 | 247 | #--------------------------------------------------------------------------- 248 | 249 | # create edge fulltext index 250 | res = graph.create_edge_fulltext_index(rel, attr) 251 | assert(res.indices_created == 1) 252 | 253 | # list indices 254 | res = graph.list_indices() 255 | assert(len(res.result_set) == 1) 256 | 257 | # drop fulltext index 258 | res = graph.drop_edge_fulltext_index(rel, attr) 259 | assert(res.indices_deleted == 1) 260 | 261 | # list indices 262 | res = graph.list_indices() 263 | assert(len(res.result_set) == 0) 264 | 265 | #--------------------------------------------------------------------------- 266 | 267 | # create edge vector index 268 | res = graph.create_edge_vector_index(rel, attr) 269 | assert(res.indices_created == 1) 270 | 271 | # list indices 272 | res = graph.list_indices() 273 | assert(len(res.result_set) == 1) 274 | 275 | # drop vector index 276 | res = graph.drop_edge_vector_index(rel, attr) 277 | assert(res.indices_deleted == 1) 278 | 279 | # list indices 280 | res = graph.list_indices() 281 | assert(len(res.result_set) == 0) 282 | 283 | -------------------------------------------------------------------------------- /falkordb/asyncio/falkordb.py: -------------------------------------------------------------------------------- 1 | import redis.asyncio as redis 2 | from .cluster import * 3 | from .graph import AsyncGraph 4 | from typing import List, Union, Optional 5 | 6 | # config command 7 | UDF_CMD = "GRAPH.UDF" 8 | LIST_CMD = "GRAPH.LIST" 9 | CONFIG_CMD = "GRAPH.CONFIG" 10 | 11 | class FalkorDB(): 12 | """ 13 | Asynchronous FalkorDB Class for interacting with a FalkorDB server. 14 | 15 | Usage example:: 16 | from falkordb.asyncio import FalkorDB 17 | # connect to the database and select the 'social' graph 18 | db = FalkorDB() 19 | graph = db.select_graph("social") 20 | 21 | # get a single 'Person' node from the graph and print its name 22 | response = await graph.query("MATCH (n:Person) RETURN n LIMIT 1") 23 | result = response.result_set 24 | person = result[0][0] 25 | print(node.properties['name']) 26 | """ 27 | 28 | def __init__( 29 | self, 30 | host='localhost', 31 | port=6379, 32 | password=None, 33 | socket_timeout=None, 34 | socket_connect_timeout=None, 35 | socket_keepalive=None, 36 | socket_keepalive_options=None, 37 | connection_pool=None, 38 | unix_socket_path=None, 39 | encoding='utf-8', 40 | encoding_errors='strict', 41 | retry_on_error=None, 42 | ssl=False, 43 | ssl_keyfile=None, 44 | ssl_certfile=None, 45 | ssl_cert_reqs='required', 46 | ssl_ca_certs=None, 47 | ssl_ca_data=None, 48 | ssl_check_hostname=False, 49 | max_connections=None, 50 | single_connection_client=False, 51 | health_check_interval=0, 52 | client_name=None, 53 | lib_name='FalkorDB', 54 | lib_version='1.0.0', 55 | username=None, 56 | retry=None, 57 | connect_func=None, 58 | credential_provider=None, 59 | protocol=2, 60 | # FalkorDB Cluster Params 61 | cluster_error_retry_attempts=3, 62 | startup_nodes=None, 63 | require_full_coverage=False, 64 | reinitialize_steps=5, 65 | read_from_replicas=False, 66 | address_remap=None, 67 | ): 68 | 69 | conn = redis.Redis(host=host, port=port, db=0, password=password, 70 | socket_timeout=socket_timeout, 71 | socket_connect_timeout=socket_connect_timeout, 72 | socket_keepalive=socket_keepalive, 73 | socket_keepalive_options=socket_keepalive_options, 74 | connection_pool=connection_pool, 75 | unix_socket_path=unix_socket_path, 76 | encoding=encoding, encoding_errors=encoding_errors, 77 | decode_responses=True, 78 | retry_on_error=retry_on_error, ssl=ssl, 79 | ssl_keyfile=ssl_keyfile, ssl_certfile=ssl_certfile, 80 | ssl_cert_reqs=ssl_cert_reqs, 81 | ssl_ca_certs=ssl_ca_certs, 82 | ssl_ca_data=ssl_ca_data, 83 | ssl_check_hostname=ssl_check_hostname, 84 | max_connections=max_connections, 85 | single_connection_client=single_connection_client, 86 | health_check_interval=health_check_interval, 87 | client_name=client_name, lib_name=lib_name, 88 | lib_version=lib_version, username=username, 89 | retry=retry, redis_connect_func=connect_func, 90 | credential_provider=credential_provider, 91 | protocol=protocol) 92 | 93 | if Is_Cluster(conn): 94 | conn = Cluster_Conn( 95 | conn, 96 | ssl, 97 | cluster_error_retry_attempts, 98 | startup_nodes, 99 | require_full_coverage, 100 | reinitialize_steps, 101 | read_from_replicas, 102 | address_remap, 103 | ) 104 | 105 | self.connection = conn 106 | self.flushdb = conn.flushdb 107 | self.execute_command = conn.execute_command 108 | 109 | @classmethod 110 | def from_url(cls, url: str, **kwargs) -> "FalkorDB": 111 | """ 112 | Creates a new FalkorDB instance from a URL. 113 | 114 | Args: 115 | cls: The class itself. 116 | url (str): The URL. 117 | kwargs: Additional keyword arguments to pass to the ``DB.from_url`` function. 118 | 119 | Returns: 120 | DB: A new DB instance. 121 | 122 | Usage example:: 123 | db = FalkorDB.from_url("falkor://[[username]:[password]]@localhost:6379") 124 | db = FalkorDB.from_url("falkors://[[username]:[password]]@localhost:6379") 125 | db = FalkorDB.from_url("unix://[username@]/path/to/socket.sock?db=0[&password=password]") 126 | """ 127 | 128 | db = cls() 129 | 130 | # switch from redis:// to falkordb:// 131 | if url.startswith('falkor://'): 132 | url = 'redis://' + url[len('falkor://'):] 133 | elif url.startswith('falkors://'): 134 | url = 'rediss://' + url[len('falkors://'):] 135 | 136 | conn = redis.from_url(url, **kwargs) 137 | db.connection = conn 138 | db.flushdb = conn.flushdb 139 | db.execute_command = conn.execute_command 140 | 141 | return db 142 | 143 | def select_graph(self, graph_id: str) -> AsyncGraph: 144 | """ 145 | Selects a graph by creating a new Graph instance. 146 | 147 | Args: 148 | graph_id (str): The identifier of the graph. 149 | 150 | Returns: 151 | AsyncGraph: A new Graph instance associated with the selected graph. 152 | """ 153 | if not isinstance(graph_id, str) or graph_id == "": 154 | raise TypeError(f"Expected a string parameter, but received {type(graph_id)}.") 155 | 156 | return AsyncGraph(self, graph_id) 157 | 158 | async def list_graphs(self) -> List[str]: 159 | """ 160 | Lists all graph names. 161 | See: https://docs.falkordb.com/commands/graph.list.html 162 | 163 | Returns: 164 | List: List of graph names. 165 | 166 | """ 167 | 168 | return await self.connection.execute_command(LIST_CMD) 169 | 170 | async def config_get(self, name: str) -> Union[int, str]: 171 | """ 172 | Retrieve a DB level configuration. 173 | For a list of available configurations see: https://docs.falkordb.com/configuration.html#falkordb-configuration-parameters 174 | 175 | Args: 176 | name (str): The name of the configuration. 177 | 178 | Returns: 179 | int or str: The configuration value. 180 | 181 | """ 182 | 183 | res = await self.connection.execute_command(CONFIG_CMD, "GET", name) 184 | return res[1] 185 | 186 | async def config_set(self, name: str, value=None) -> None: 187 | """ 188 | Update a DB level configuration. 189 | For a list of available configurations see: https://docs.falkordb.com/configuration.html#falkordb-configuration-parameters 190 | 191 | Args: 192 | name (str): The name of the configuration. 193 | value: The value to set. 194 | 195 | Returns: 196 | None 197 | 198 | """ 199 | 200 | return await self.connection.execute_command(CONFIG_CMD, "SET", name, value) 201 | 202 | # GRAPH.UDF LOAD [REPLACE]