# frozen_string_literal: true module Boltless module Extensions # A top-level gem-module extension for neo4j operations. # # rubocop:disable Metrics/BlockLength because this is how # an +ActiveSupport::Concern+ looks like module Operations extend ActiveSupport::Concern class_methods do # Clear everything from the neo4j database, including indexes, # constraints, all nodes and all relationships. Afterwards the # database is completely empty and "unconfigured". (dist clean) # # @param database [String, Symbol] the neo4j database to use # # rubocop:disable Metrics/MethodLength because of # multiple transactions # rubocop:disable Metrics/AbcSize because of the extra transaction # handlings (we cannot do multiple structural changes in a single # transaction) def clear_database!(database: Boltless.configuration.default_db) logger.warn('Clear neo4j database ..') # Clear all constraints, this is a schema # modification so we have to run it in a separate transaction transaction!(database: database) do |tx| constraint_names.each do |name| logger.info(" > Drop neo4j constraint #{name} ..") tx.run!("DROP CONSTRAINT #{name}") end end # Clear all indexes, this is a schema # modification so we have to run it in a separate transaction transaction!(database: database) do |tx| index_names.each do |name| logger.info(" > Drop neo4j index #{name} ..") tx.run!("DROP INDEX #{name}") end end # Afterwards clear all nodes and relationships stats = write!('MATCH (n) DETACH DELETE n', database: database, with_stats: true).stats logger.info(" > Nodes deleted: #{stats[:nodes_deleted]}") logger.info( " > Relationships deleted: #{stats[:relationship_deleted]}" ) end # rubocop:enable Metrics/MethodLength # rubocop:enable Metrics/AbcSize # Check whenever the given name is already taken on the neo4j # database for a component (index or constraint). # # @param database [String, Symbol] the neo4j database to use # @param name [String, Symbol] the name to check # @return [Boolean] whenever the name is taken/present or not def component_name_present?(name, database: Boltless.configuration.default_db) pool = index_names(database: database) + constraint_names(database: database) pool.include? name.to_s end # List all currently available indexes by their names. # # @param database [String, Symbol] the neo4j database to use # @return [Array] the index names def index_names(database: Boltless.configuration.default_db) res = query!('SHOW INDEXES YIELD name', database: database).pluck(:name) res - constraint_names end # List all currently available constraints by their names. # # @param database [String, Symbol] the neo4j database to use # @return [Array] the constraint names def constraint_names(database: Boltless.configuration.default_db) query!('SHOW CONSTRAINTS YIELD name', database: database).pluck(:name) end # Create a new index at the neo4j database. # # @see https://bit.ly/3GawzVP # @param name [String, Symbol] the name of the index, we do not # allow autogenerated names # @param type [String, Symbol] the index type (+:btree+, +:lookup+, # +:text+, +:range+, +:point+, +:fulltext+) # @param options [Hash{Symbol => Mixed}] additional index options # for neo4j # @param for [String] the node/property query for matching # @param on [String] the collection of nodes/properties to index # @param database [String, Symbol] the neo4j database to use # # rubocop:disable Metrics/ParameterLists because of the various # configuration options of a neo4j index def add_index(name:, for:, on:, type: :btree, options: nil, database: Boltless.configuration.default_db) # CREATE TEXT INDEX [index_name] IF NOT EXISTS # FOR ()-[r:TYPE_NAME]-() # ON (r.propertyName) # OPTIONS {option: value[, ...]} # Assemble and perform the neo4j query type = type == :btree ? '' : type.to_s.upcase options = options.blank? ? '' : "OPTIONS #{to_options(options)}" write <<~CYPHER, database: database CREATE #{type} INDEX #{name} IF NOT EXISTS FOR #{binding.local_variable_get(:for)} ON #{on} #{options} CYPHER end # rubocop:enable Metrics/ParameterLists # Drop an index from the neo4j database. # # @param name [String, Symbol] the name of the index # @param database [String, Symbol] the neo4j database to use def drop_index(name, database: Boltless.configuration.default_db) write!("DROP INDEX #{name} IF EXISTS", database: database) end # Create a new constraint at the neo4j database. # # Adding constraints is an atomic operation that can take a while. # All existing data has to be scanned before neo4j can turn the # constraint 'on'. # # @see https://bit.ly/3GawzVP # # @param name [String, Symbol] the name of the index, we do not # allow autogenerated names # @param options [Hash{Symbol => Mixed}] additional index options # for neo4j # @param for [String] the node/property query for matching # @param require [String] the constraint to apply # @param database [String, Symbol] the neo4j database to use def add_constraint(name:, for:, require:, options: nil, database: Boltless.configuration.default_db) # CREATE CONSTRAINT [constraint_name] IF NOT EXISTS # FOR (n:LabelName) # REQUIRE n.propertyName IS UNIQUE # OPTIONS { option: value[, ...] } # Assemble and perform the neo4j query options = options.blank? ? '' : "OPTIONS #{to_options(options)}" write! <<~CYPHER, database: database CREATE CONSTRAINT #{name} IF NOT EXISTS FOR #{binding.local_variable_get(:for)} REQUIRE #{binding.local_variable_get(:require)} #{options} CYPHER end # Drop a constraint from the neo4j database. # # @param name [String, Symbol] the name of the constraint # @param database [String, Symbol] the neo4j database to use def drop_constraint(name, database: Boltless.configuration.default_db) write!("DROP CONSTRAINT #{name} IF EXISTS", database: database) end end end # rubocop:enable Metrics/BlockLength end end