Bump sequel from 5.105.0 to 5.107.0 - #1030
Open
dependabot[bot] wants to merge 1 commit into
Open
Conversation
Bumps [sequel](https://github.com/jeremyevans/sequel) from 5.105.0 to 5.107.0. - [Changelog](https://github.com/jeremyevans/sequel/blob/master/CHANGELOG) - [Commits](jeremyevans/sequel@5.105.0...5.107.0) --- updated-dependencies: - dependency-name: sequel dependency-version: 5.107.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com>
Contributor
1 similar comment
Contributor
Contributor
gem compare sequel 5.105.0 5.107.0Compared versions: ["5.105.0", "5.107.0"]
DIFFERENT rubygems_version:
5.105.0: 4.0.10
5.107.0: 4.0.16
DIFFERENT version:
5.105.0: 5.105.0
5.107.0: 5.107.0
DIFFERENT files:
5.105.0->5.107.0:
* Changed:
lib/sequel/adapters/shared/postgres.rb +987/-4
lib/sequel/adapters/shared/sqlite.rb +5/-4
lib/sequel/database/schema_generator.rb +3/-1
lib/sequel/dataset/sql.rb +1/-0
lib/sequel/sql.rb +3/-0
lib/sequel/version.rb +1/-1 |
Contributor
Contributor
gem compare --diff sequel 5.105.0 5.107.0Compared versions: ["5.105.0", "5.107.0"]
DIFFERENT files:
5.105.0->5.107.0:
* Changed:
lib/sequel/adapters/shared/postgres.rb
--- /tmp/d20260805-1551-hhqd62/sequel-5.105.0/lib/sequel/adapters/shared/postgres.rb 2026-08-05 02:33:32.033018220 +0000
+++ /tmp/d20260805-1551-hhqd62/sequel-5.107.0/lib/sequel/adapters/shared/postgres.rb 2026-08-05 02:33:32.083018254 +0000
@@ -258,0 +259,558 @@
+ module PropertyGraph
+ # Base class for all Generator DSL classes. This uses a design where
+ # The DSL class is only used for the evaluation of the block, and new
+ # returns a frozen struct.
+ class Generator
+ # Instead of returning the Generator instance, return a frozen struct
+ # with data from the generator. This prevents accidentally calling the
+ # generator methods, and makes it possible for the generator class and
+ # result class to use the same method name in two different ways, with
+ # the generator setting data and the frozen struct method returning it.
+ # The frozen struct classes use the constant Data under each generator
+ # subclass.
+ def self.new(*args, &block)
+ super(*args, &block).data
+ end
+
+ # Base class for Vertex and Edge.
+ class Element < self
+ Data = Struct.new(:name, :key, :labels)
+
+ # +name+ specifies the name of the vertex or edge. It can be an
+ # SQL::AliasedExpression to use an alias. Options:
+ # :properties :: Specifies fixed properties for the vertex or edge.
+ # If this is given, you cannot use the label method
+ # inside the block.
+ def initialize(name, opts=OPTS, &block)
+ @name = name
+ @labels = []
+ if opts.key?(:properties)
+ @labels << [nil, opts[:properties]].freeze
+ @labels.freeze
+ end
+ instance_exec(&block) if block
+ @labels.freeze
+ freeze
+ end
+
+ def data
+ Data.new(@name, @key, @labels).freeze
+ end
+
+ # Set the column(s) to use for the KEY clause, which are the columns
+ # that uniquely identify rows in the table:
+ #
+ # key(:id)
+ # # KEY (id)
+ #
+ # key([:id1, :id2])
+ # # KEY (id1, id2)
+ def key(columns)
+ @key = Array(columns)
+ end
+
+ # Add a label and properties for the label for this vertex/edge.
+ # A vertex or edge can have multiple labels with separate properties,
+ # if it wasn't created with fixed properties. The +name+ argument
+ # specifies the label name. The +properties+ argument specifies the
+ # properties:
+ # nil, :all :: PROPERTIES ALL COLUMNS
+ # false, :none, [] :: NO PROPERTIES
+ # Array :: Array of specific properties. Each element should be a Symbol,
+ # SQL::Identifier, or SQL::AliasedExpression.
+ #
+ # label(:label_name)
+ # # LABEL label_name PROPERTIES ALL COLUMNS
+ #
+ # label(:label_name, [])
+ # # LABEL label_name NO PROPERTIES
+ #
+ # label(:label_name, [:c, Sequel[:b].as(:d)], Sequel[:e])
+ # # LABEL label_name PROPERTIES (c, b AS d, e)
+ def label(name, properties=:all)
+ if @labels.frozen?
+ raise Error, "cannot specify label for property graph vertex or edge with fixed properties"
+ end
+ @labels << [name, properties].freeze
+ nil
+ end
+ end
+
+ # Vertex is used for the block passed to Create#vertex, used to configure
+ # vertices in the property graph. It doesn't have any additional behavior
+ # compared to the Element class, so this is an alias instead of a subclass.
+ Vertex = Element
+
+ # Target is used for the block passed to Edge#source and Edge#destination,
+ # used to configure the source and destination of property graph edges.
+ class Target < self
+ Data = Struct.new(:name, :key, :references)
+
+ # +name+ specifies the name of the source or destination.
+ def initialize(name, &block)
+ @name = name
+ @key = nil
+ @references = nil
+ instance_exec(&block) if block
+ freeze
+ end
+
+ def data
+ Data.new(@name, @key, @references).freeze
+ end
+
+ # Set the column(s) to use for the KEY clause, which are the columns
+ # in the edge table that reference columns in the source or destination.
+ # Should be combined with #references to specify the columns being
+ # referenced.
+ #
+ # key(:vertex_id)
+ # # KEY (vertex_id)
+ #
+ # key([:vertex_id1, :vertex_id2])
+ # # KEY (vertex_id1, vertex_id2)
+ def key(keys)
+ @key = Array(keys)
+ end
+
+ # Set the column(s) to use for the REFERENCES clause, which are the columns
+ # in the source or destination table that are referenced by the edge table.
+ # Should be combined with #key to specify the columns doing the referencing.
+ #
+ # references(:id)
+ # # REFERENCES (id)
+ #
+ # references([:id1, :id2])
+ # # REFERENCES (id1, id2)
+ def references(refs)
+ @references = Array(refs)
+ end
+ end
+
+ # Edge is used for block passed to Create#edge, used to configure edges
+ # in the property graph.
+ class Edge < Element
+ Data = Struct.new(:name, :key, :labels, :source, :destination)
+
+ # In addition to inherited behavior, raises an error if a block
+ # is not passed or source or destination is not called in the block.
+ def initialize(name, opts=OPTS, &block)
+ super
+
+ unless @source && @destination
+ raise Error, "source and/or destination not defined for property graph edge"
+ end
+ end
+
+ def data
+ Data.new(@name, @key, @labels, @source, @destination).freeze
+ end
+
+ # Specify the source for the edge, with block evaluted by Target.
+ def source(name, &block)
+ raise Error, "cannot specify multiple sources for a property graph edge" if @source
+ @source = Target.new(name, &block)
+ end
+
+ # Specify the destination for the edge, with block evaluted by Target.
+ def destination(name, &block)
+ raise Error, "cannot specify multiple destinations for a property graph edge" if @destination
+ @destination = Target.new(name, &block)
+ end
+ end
+
+ # Create is used to evaluate the block given to DatabaseMethods#create_property_graph,
+ # used to specify the vertices and edges in the property graph.
+ class Create < self
+ Data = Struct.new(:vertices, :edges)
+
+ def initialize(&block)
+ @vertices = []
+ @edges = []
+ instance_exec(&block)
+ @vertices.freeze
+ @edges.freeze
+ freeze
+ end
+
+ def data
+ Data.new(@vertices, @edges).freeze
+ end
+
+ # Adds a vertex to the property graph, with the block evaluted by Vertex.
+ def vertex(name, opts=OPTS, &block)
+ @vertices << Vertex.new(name, opts, &block)
+ end
+
+ # Adds an edge to the property graph, with the block evaluted by Edge.
+ def edge(name, opts=OPTS, &block)
+ @edges << Edge.new(name, opts, &block)
+ end
+ end
+
+ # AlterElement is used to evaluate the block passed to
+ # Alter#alter_vertex_table and Alter#alter_edge_table.
+ class AlterElement < self
+ # +kind+ is +:vertex+ or +:edge+. +name+ is the alias of the
+ # vertex or edge table to alter.
+ def initialize(kind, name, &block)
+ @kind = kind
+ @name = name
+ @labels = []
+ @operations = []
+ instance_exec(&block)
+
+ # All labels added via #add_label are combined into a single
+ # ADD LABEL operation, as PostgreSQL supports adding multiple
+ # labels in a single ALTER ... ADD LABEL statement.
+ unless @labels.empty?
+ @operations << {:op=>:add_label, :kind=>kind, :name=>name, :labels=>@labels.freeze}
+ end
+
+ @operations.each(&:freeze)
+ @operations.freeze
+ freeze
+ end
+
+ def data
+ @operations
+ end
+
+ # Add a label (and optional properties) to the vertex/edge table.
+ # Takes the same arguments as Element#label. Can be called multiple
+ # times to add multiple labels.
+ #
+ # add_label(:l)
+ # # ADD LABEL l PROPERTIES ALL COLUMNS
+ def add_label(name, properties=:all)
+ @labels << [name, properties].freeze
+ nil
+ end
+
+ # Remove a label from the vertex/edge table. Options:
+ # :cascade :: Use CASCADE to drop dependent objects.
+ #
+ # drop_label(:l)
+ # # DROP LABEL l
+ def drop_label(name, opts=OPTS)
+ @operations << {:op=>:drop_label, :kind=>@kind, :name=>@name, :label=>name, :cascade=>opts[:cascade]}
+ nil
+ end
+
+ # Add properties to an existing label on the vertex/edge table.
+ # +properties+ is an expression, or array of expressions, the same
+ # as the explicit array form of the +properties+ argument to
+ # Element#label.
+ #
+ # add_properties(:l, [:c1, Sequel[:c2].as(:c3)])
+ # # ALTER LABEL l ADD PROPERTIES (c1, c2 AS c3)
+ def add_properties(label, properties)
+ @operations << {:op=>:add_properties, :kind=>@kind, :name=>@name, :label=>label, :properties=>Array(properties)}
+ nil
+ end
+
+ # Remove properties from an existing label on the vertex/edge table.
+ # +properties+ is a column name, or array of column names. Options:
+ # :cascade :: Use CASCADE to drop dependent objects.
+ #
+ # drop_properties(:l, [:c1])
+ # # ALTER LABEL l DROP PROPERTIES (c1)
+ def drop_properties(label, properties, opts=OPTS)
+ @operations << {:op=>:drop_properties, :kind=>@kind, :name=>@name, :label=>label, :properties=>Array(properties), :cascade=>opts[:cascade]}
+ nil
+ end
+ end
+
+ # Alter is used to evaluate the block given to DatabaseMethods#alter_property_graph,
+ # used to specify changes to an existing property graph.
+ class Alter < self
+ def initialize(&block)
+ @operations = []
+ instance_exec(&block)
+
+ @operations.each do |op|
+ case op[:op]
+ when :add_vertex_tables, :add_edge_tables
+ op[:tables].freeze
+ end
+ op.freeze
+ end
+ @operations.freeze
+ freeze
+ end
+
+ def data
+ @operations
+ end
+
+ # Add a vertex to the property graph, with the block used to configure the
+ # vertex.
+ #
+ # alter_property_graph.add_vertex(:v)
+ # # ADD VERTEX TABLES (v)
+ def add_vertex(name, opts=OPTS, &block)
+ add_tables_operation(:add_vertex_tables) << Vertex.new(name, opts, &block)
+ nil
+ end
+
+ # Add an edge to the property graph, with the block used to configure the edge.
+ #
+ # alter_property_graph.add_edge(:e){source :v1; destination :v2}
+ # # ADD EDGE TABLES (e SOURCE v1 DESTINATION v2)
+ def add_edge(name, opts=OPTS, &block)
+ add_tables_operation(:add_edge_tables) << Edge.new(name, opts, &block)
+ nil
+ end
+
+ # Remove vertex tables (referenced by their aliases) from the
+ # property graph. +aliases+ can be a single alias or an array.
+ # Options:
+ # :cascade :: Use CASCADE instead of the default RESTRICT.
+ #
+ # alter_property_graph.drop_vertex_tables([:v1, :v2])
+ # # DROP VERTEX TABLES (v1, v2)
+ def drop_vertex_tables(aliases, opts=OPTS)
+ @operations << {:op=>:drop_vertex_tables, :aliases=>Array(aliases), :cascade=>opts[:cascade]}
+ nil
+ end
+
+ # Remove edge tables (referenced by their aliases) from the property
+ # graph. See #drop_vertex_tables.
+ #
+ # alter_property_graph.drop_edge_tables([:e1, :e2])
+ # # DROP EDGE TABLES (e1, e2)
+ def drop_edge_tables(aliases, opts=OPTS)
+ @operations << {:op=>:drop_edge_tables, :aliases=>Array(aliases), :cascade=>opts[:cascade]}
+ nil
+ end
+
+ # Modify an existing vertex table (referenced by its alias).
+ #
+ # alter_property_graph.alter_vertex_table(:v){add_label :l}
+ # # ALTER VERTEX TABLE v ADD LABEL l PROPERTIES ALL COLUMNS
+ def alter_vertex_table(name, &block)
+ @operations.concat(AlterElement.new(:vertex, name, &block))
+ nil
+ end
+
+ # Modify an existing edge table (referenced by its alias).
+ #
+ # alter_property_graph.alter_edge_table(:e, properties: :none){drop_label :l}
+ # # ALTER VERTEX TABLE e DROP LABEL l
+ def alter_edge_table(name, &block)
+ @operations.concat(AlterElement.new(:edge, name, &block))
+ nil
+ end
+
+ # Change the owner of the property graph. +new_owner+ is usually a
+ # Symbol or SQL::Identifier for the role name, but can be
+ # <tt>Sequel.lit('CURRENT_USER')</tt> or
+ # <tt>Sequel.lit('SESSION_USER')</tt>.
+ #
+ # alter_property_graph.owner_to(:new_owner)
+ # # OWNER TO new_owner
+ def set_owner(new_owner)
+ @operations << {:op=>:set_owner, :owner=>new_owner}
+ nil
+ end
+
+ private
+
+ # Internals of add_vertex and add_edge.
+ def add_tables_operation(op_name)
+ unless op = @operations.find{|o| o[:op] == op_name}
+ @operations << (op = {:op=>op_name, :tables=>[]})
+ end
+ op[:tables]
+ end
+ end
+ end
+
+ # Represents a GRAPH_TABLE expression, used to query a property graph
+ # via graph pattern matching. This is used in place of a table name
+ # expression or dataset in a SELECT query. These are created by calling
+ # #graph_table on the related Database object.
+ #
+ # Table uses a method chaining design, similar to Dataset, where methods
+ # return modified frozen copies of the object.
+ class Table
+ include SQL::AliasMethods
+
+ # Internal struct for a single element (vertex or edge) in the graph pattern:
+ # +type+ :: Either :vertex or :edge.
+ # +marker+ :: Connector string to use for the element (empty for initial vertex).
+ # +label+ :: Label restriction symbol or SQL::Identifier for the element, if any.
+ # Can be an array or set to match multiple labels.
+ # +var+ :: Graph pattern variable symbol for the element, if any.
+ # +where+ :: WHERE condition for the element, if any.
+ Element = Struct.new(:type, :marker, :label, :var, :where) do
+ # Method used to create elements, used instead of new
+ # to ensure that the returned elements are frozen.
+ def self.create(type, marker, label, opts)
+ case label
+ when Array, Set
+ label = label.dup.freeze unless label.frozen?
+ end
+
+ case where = opts[:where]
+ when Hash, Array
+ where = SQL::BooleanExpression.from_value_pairs(where)
+ end
+
+ new(type, marker, label, opts[:var], where).freeze
+ end
+
+ private_class_method :new
+ end
+ private_constant :Element
+
+ # The name of the property graph the table is querying.
+ attr_reader :name
+
+ # A frozen array of Element instances, representing the vertices and
+ # edges in the graph pattern.
+ attr_reader :elements
+
+ # A frozen array of the columns used in the COLUMNS clause (aliased
+ # as columns_used, as #columns is used to modify the columns).
+ attr_reader :columns
+ alias columns_used columns
+
+ # Create a new Table with the given +graph_name+, with +initial_vertex_label+
+ # and +initial_vertex_opts+ being used to create the initial vertex.
+ # See Table#link for which options are supported for the initial vertex.
+ def self.create(graph_name, initial_vertex_label, initial_vertex_opts)
+ vertex = Element.create(:vertex, "", initial_vertex_label, initial_vertex_opts)
+ new(graph_name, [vertex].freeze, [].freeze)
+ end
+
+ def initialize(name, elements, columns)
+ @name = name
+ @elements = elements
+ @columns = columns
+ freeze
+ end
+
+ # Return a modified copy with an element added using a bidirectional link
+ # (<tt>-</tt> in the graph pattern).
+ # +label+ specifies the label restriction for the element. This can be
+ # nil for no label restriction, or an array or set to restrict to the
+ # given labels.
+ #
+ # Options supported:
+ # +:var+ :: Specifies a graph pattern variable name for the element,
+ # usable in the WHERE or COLUMNS clauses.
+ # +:vertex+ :: Specifies that the element being linked to is a vertex.
+ # This allows for direct vertex<->vertex linking, instead of
+ # the default vertex<->edge<->vertex linking.
+ # +:where+ :: An expression to use for the WHERE clause for the element.
+ #
+ # DB.graph_table(:gn, :v).link(:e)
+ # # GRAPH_TABLE (gn MATCH (IS v)-[IS e])
+ def link(label, opts=OPTS)
+ append_element('-', label, opts)
+ end
+
+ # Similar to #link, but uses a directed link from the previous element
+ # to the new element (<tt>-></tt> in the graph pattern). Accepts same
+ # arguments and options as #link.
+ #
+ # DB.graph_table(:gn, :v).to(:e)
+ # # GRAPH_TABLE (gn MATCH (IS v)->[IS e])
+ def to(label, opts=OPTS)
+ append_element('->', label, opts)
+ end
+
+ # Similar to #link, but uses a directed link from the new element
+ # to the previous element (<tt><-</tt> in the graph pattern). Accepts
+ # same arguments and options as #link.
+ #
+ # DB.graph_table(:gn, :v).from(:e)
+ # # GRAPH_TABLE (gn MATCH (IS v)<-[IS e])
+ def from(label, opts=OPTS)
+ append_element('<-', label, opts)
+ end
+
+ # Return a modifies copy that uses the given columns. A graph table
+ # must have a least one column set before it is used in a query.
+ #
+ # DB.graph_table(:gn, :v).columns(:a, Sequel[:b].as(:c))
+ # # GRAPH_TABLE (gn MATCH (IS v) COLUMNS (a, b AS c))
+ def columns(*cols)
+ self.class.new(@name, @elements, cols.freeze)
+ end
+
+ # Return a modified copy that adds the given columns to the existing
+ # list of columns for the graph table.
+ def add_columns(*cols)
+ columns(*@columns, *cols)
+ end
+
+ # Append the SQL for the GRAPH_TABLE expression to the given SQL string.
+ # Requires graph table have at least one column set.
+ def sql_literal_append(ds, sql)
+ if @columns.empty?
+ raise Error, "cannot use graph_table in a query if it does not return any columns"
+ end
+ if @elements.last.type == :edge
+ raise Error, "cannot use graph_table in a query if the last element is an edge"
+ end
+
+ sql << "GRAPH_TABLE ("
+ ds.literal_append(sql, @name)
+ sql << " MATCH "
+
+ @elements.each do |element|
+ marker = element.marker
+ var = element.var
+ label = element.label
+ where = element.where
+ vertex = element.type == :vertex
+
+ sql << marker
+ sql << (vertex ? '(' : '[')
+
+ ds.literal_append(sql, var) if var
+ if label
+ sql << (var ? " IS " : "IS ")
+ if label.is_a?(Array)
+ label_sep = ""
+ label.each do |l|
+ sql << label_sep
+ label_sep = "|" if label_sep.empty?
+ ds.literal_append(sql, l)
+ end
+ else
+ ds.literal_append(sql, label)
+ end
+ end
+
+ if where
+ sql << ((var || label) ? " WHERE " : "WHERE ")
+ ds.literal_append(sql, where)
+ end
+
+ sql << (vertex ? ')' : ']')
+ end
+
+ sql << " COLUMNS "
+ ds.literal_append(sql, @columns)
+ sql << ")"
+ end
+
+ private
+
+ # Internals of #link, #to, and #from.
+ def append_element(marker, label, opts)
+ node_type = if opts[:vertex]
+ :vertex
+ else
+ @elements.last.type == :vertex ? :edge : :vertex
+ end
+
+ element = Element.create(node_type, marker, label, opts)
+ self.class.new(@name, (@elements.dup << element).freeze, @columns)
+ end
+ end
+ end
+
@@ -340,0 +899,58 @@
+ # Alter the property graph with the given +name+, supported on PostgreSQL 19+.
+ # The block uses a DSL, evaluated by PropertyGraph::Generator::Alter. Example:
+ #
+ # DB.alter_property_graph(:my_graph) do
+ # # PropertyGraph::Generator::Alter
+ # add_vertex :companies2
+ # # ALTER PROPERTY GRAPH "my_graph" ADD VERTEX TABLES ("companies2")
+ #
+ # add_edge :works_at2 do
+ # # PropertyGraph::Generator::Edge
+ # source :people
+ # destination :companies2
+ # end
+ # # ALTER PROPERTY GRAPH "my_graph" ADD EDGE TABLES
+ # # ("works_at2" SOURCE "people" DESTINATION "companies2")
+ #
+ # drop_vertex_tables [:p2], cascade: true
+ # # ALTER PROPERTY GRAPH "my_graph" DROP VERTEX TABLES ("p2") CASCADE
+ #
+ # drop_edge_tables :e2
+ # # ALTER PROPERTY GRAPH "my_graph" DROP EDGE TABLES ("e2")
+ #
+ # alter_vertex_table :companies do
+ # # PropertyGraph::Generator::AlterElement
+ # add_label :public_company, [:name, :symbol]
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER VERTEX TABLE "companies"
+ # # ADD LABEL "public_company" PROPERTIES ("name", "symbol")
+ #
+ # drop_label :private_company
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER VERTEX TABLE "companies"
+ # # DROP LABEL "private_company"
+ #
+ # add_properties :company, :revenue
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER VERTEX TABLE "companies"
+ # # ALTER LABEL "company" ADD PROPERTIES ("revenue")
+ #
+ # drop_properties :company, :internal_id, cascade: true
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER VERTEX TABLE "companies"
+ # # ALTER LABEL "company" DROP PROPERTIES ("internal_id") CASCADE
+ # end
+ #
+ # alter_edge_table :works_at do
+ # # PropertyGraph::Generator::AlterElement
+ # add_label :employment
+ # end
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER EDGE TABLE "works_at"
+ # # ADD LABEL "employment" PROPERTIES ALL COLUMNS
+ #
+ # owner_to :new_owner
+ # # ALTER PROPERTY GRAPH "my_graph" OWNER TO "new_owner"
+ # end
+ def alter_property_graph(name, &block)
+ PropertyGraph::Generator::Alter.new(&block).each do |op|
+ execute_ddl(alter_property_graph_op_sql(name, op).freeze)
+ end
+ nil
+ end
+
@@ -469,0 +1086,61 @@
+ # Create a property graph in the database, supported on PostgreSQL 19+.
+ #
+ # Arguments:
+ # name :: Name of the property graph
+ # opts :: options hash:
+ # :temp :: Create the property graph as a temporary property graph.
+ #
+ # The block uses a DSL, with classes under PropertyGraph::Generator:
+ #
+ # DB.create_property_graph(:my_graph) do
+ # # PropertyGraph::Generator::Create
+ # vertex :people
+ #
+ # vertex Sequel.as(:people, :p), properties: []
+ #
+ # vertex Sequel.as(:companies, :c) do
+ # # PropertyGraph::Generator::Vertex
+ # key :id
+ # label :company
+ # label :c, [:name, (Sequel[:revenue] / 1000).as(:revenue_thousands)]
+ # end
+ #
+ # edge :works_at do
+ # # PropertyGraph::Generator::Edge
+ # source :people
+ # destination :c
+ # end
+ #
+ # edge Sequel.as(:employment, :e) do
+ # source :people do
+ # # PropertyGraph::Generator::Target
+ # key :person_id
+ # references :id
+ # end
+ # destination :c do
+ # # PropertyGraph::Generator::Target
+ # key :company_id
+ # references :id
+ # end
+ # label :employment
+ # end
+ # end
+ # # CREATE PROPERTY GRAPH "my_graph"
+ # # VERTEX TABLES (
+ # # "people",
+ # # "people" AS "p" NO PROPERTIES,
+ # # "companies" AS "c" KEY ("id")
+ # # LABEL "company" PROPERTIES ALL COLUMNS
+ # # LABEL "c" PROPERTIES ("name", ("revenue" / 1000) AS "revenue_thousands"))
+ # # EDGE TABLES (
+ # # "works_at"
+ # # SOURCE "people"
+ # # DESTINATION "c",
+ # # "employment" AS "e"
+ # # SOURCE KEY ("person_id") REFERENCES "people" ("id")
+ # # DESTINATION KEY ("company_id") REFERENCES "c" ("id")
+ # # LABEL "employment" PROPERTIES ALL COLUMNS)
+ def create_property_graph(name, opts=OPTS, &block)
+ execute_ddl(create_property_graph_sql(name, PropertyGraph::Generator::Create.new(&block), opts))
+ end
+
@@ -566,0 +1244,9 @@
+ # Drops a property graph from the database. Arguments:
+ # name :: name of the property graph to drop
+ # opts :: options hash:
+ # :cascade :: Drop other objects depending on this property_graph.
+ # :if_exists :: Don't raise an error if the property graph doesn't exist.
+ def drop_property_graph(name, opts=OPTS)
+ self << drop_property_graph_sql(name, opts).freeze
+ end
+
@@ -653,0 +1340,76 @@
+ # Return a PropertyGraph::Table instance for a property graph search
+ # (a GRAPH_TABLE clause for a SELECT query). Supported on PostgreSQL 19+.
+ #
+ # Arguments:
+ # +property_graph_name+ :: The property graph to query
+ # +initial_vertex_label+ :: The label restriction for the initial vertex for the
+ # graph pattern (can be nil for no label, or an array
+ # or set for restricting to one of multiple labels).
+ # +initial_vertex_opts+ :: The options for the initial vertex, see
+ # PropertyGraph::Table#link for available options.
+ #
+ # The returned instance should be further modified by calling methods on it,
+ # using a similar approach to how datasets work, where the methods return a
+ # modified copy of the receiver. The available methods:
+ #
+ # link :: Add a bidirectional link to a new element (vertex or edge)
+ # to :: Add a directional link from the last element to the new element
+ # from :: Add a direciton link from the new element to last element
+ # columns :: Replace the columns the graph table returns
+ # add_columns :: Append to the columns the graph table returns.
+ #
+ # See PropertyGraph::Table for the details of these methods and the arguments
+ # and options they support. Note that for a graph table to be usable in a query,
+ # it must return at least one column, and the last element in the graph pattern
+ # must be a vertex.
+ #
+ # gt = DB.graph_table(:pgn, :iv)
+ # # Not yet usable, does not return any columns
+ #
+ # # Set columns for graph table
+ # gt = gt.columns(:c, Sequel[1].as(:d))
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv") COLUMNS ("c", 1 AS "d"))
+ #
+ # # Adds directional link to edge, since last (initial) element was a vertex
+ # gt = gt.link(:e1)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"] COLUMNS ("c", 1 AS "d"))
+ #
+ # # Adds directional link from edge to vertex, since last element was an edge
+ # gt = gt.to(:v2)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2") COLUMNS ("c", 1 AS "d"))
+ #
+ # # Adds bidirection link from vertex to vertex (overriding the default)
+ # gt = gt.link(:v3, vertex: true)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3") COLUMNS ("c", 1 AS "d"))
+ #
+ # # Adds directional link from new edge to last vertex, since last element was an vertex.
+ # # Sets graph pattern variable name and uses it in a WHERE clause for the added element.
+ # gt = gt.from(:e2, var: :a2, where: {Sequel[:a2][:c] => 1})
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3")
+ # # <-["a2" IS "e2" WHERE ("a2"."c" = 1)] COLUMNS ("c", 1 AS "d"))
+ #
+ # # Can use nil as a label for no label restriction, both with and without a variable name
+ # gt = gt.to(nil).to(nil, var: :a3)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3")
+ # # <-["a2" IS "e2" WHERE ("a2"."c" = 1)]->[]->("a3") COLUMNS ("c", 1 AS "d"))
+ #
+ # # Can restrict to a one of a set of labels
+ # gt = gt.from([:x, :y], var: :a6)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3")
+ # # <-["a2" IS "e2" WHERE ("a2"."c" = 1)]->[]->("a3")->["a6" IS "x"|"y"] COLUMNS ("c", 1 AS "d"))
+ #
+ # # Add column(s) to the graph table
+ # gt = gt.add_columns(:y)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3")
+ # # <-["a2" IS "e2" WHERE ("a2"."c" = 1)]->[]->("a3")->["a6" IS "x"|"y"]
+ # # COLUMNS ("c", 1 AS "d", "y"))
+ #
+ # DB.from(gt)
+ # # SELECT * FROM GRAPH_TABLE (...)
+ #
+ # DB.from(:x).cross_join(gt)
+ # # SELECT * FROM "x" CROSS JOIN GRAPH_TABLE (...)
+ def graph_table(property_graph_name, initial_vertex_label, initial_vertex_opts=OPTS)
+ PropertyGraph::Table.create(property_graph_name, initial_vertex_label, initial_vertex_opts)
+ end
+
@@ -743,0 +1506,22 @@
+ # Array of symbols specifying property graphs in the current database.
+ # The dataset used is yielded to the block if one is provided,
+ # otherwise, an array of symbols of property graph names is returned.
+ # Supported on PostgreSQL 19+, will be an empty array on lower versions.
+ #
+ # Options:
+ # :qualify :: Return the property graph names as Sequel::SQL::QualifiedIdentifier
+ # instances, using the schema the property graph is located in as the qualifier.
+ # :schema :: The schema to search
+ # :server :: The server to use
+ def property_graphs(opts=OPTS, &block)
+ pg_class_relname('g', opts, &block)
+ end
+
+ # Rename a property graph.
+ #
+ # DB.rename_property_graph(:x, :y)
+ # # ALTER PROPERTY GRAPH x RENAME TO y
+ def rename_property_graph(old_name, new_name)
+ execute_ddl("ALTER PROPERTY GRAPH #{literal(old_name)} RENAME TO #{literal(new_name)}".freeze)
+ end
+
@@ -806,0 +1591,10 @@
+ # Change the schema for a property graph. Options:
+ # :if_exists :: Use the IF EXISTS clause to not raise an error if the
+ # property graph does not exist.
+ #
+ # DB.set_property_graph_schema(:x, :y)
+ # # ALTER PROPERTY GRAPH x SET SCHEMA y
+ def set_property_graph_schema(old_name, new_name, opts=OPTS)
+ execute_ddl("ALTER PROPERTY GRAPH#{" IF EXISTS" if opts[:if_exists]} #{literal(old_name)} SET SCHEMA #{literal(new_name)}".freeze)
+ end
+
@@ -1206,0 +2001,56 @@
+ # SQL statement for a single ALTER PROPERTY GRAPH operation.
+ def alter_property_graph_op_sql(name, op)
+ sql = String.new << "ALTER PROPERTY GRAPH " << quote_schema_table(name) << " "
+
+ case op_type = op[:op]
+ when :add_vertex_tables
+ sql << "ADD VERTEX TABLES (" <<
+ op[:tables].map do |vertex|
+ create_property_graph_table_sql(vertex) <<
+ create_property_graph_labels_sql(vertex.labels)
+ end.join(', ') << ")"
+ when :add_edge_tables
+ sql << "ADD EDGE TABLES (" <<
+ op[:tables].map do |edge|
+ create_property_graph_table_sql(edge) <<
+ " SOURCE " << create_property_graph_edge_side_sql(edge.source) <<
+ " DESTINATION " << create_property_graph_edge_side_sql(edge.destination) <<
+ create_property_graph_labels_sql(edge.labels)
+ end.join(', ') << ")"
+ when :drop_vertex_tables, :drop_edge_tables
+ sql << (op_type == :drop_vertex_tables ? "DROP VERTEX TABLES " : "DROP EDGE TABLES ") <<
+ literal(op[:aliases])
+ when :add_label
+ sql << alter_property_graph_element_table_sql(op)
+ op[:labels].each do |label_name, properties|
+ sql << " ADD LABEL " << quote_identifier(label_name) <<
+ create_property_graph_properties_clause_sql(properties)
+ end
+ when :drop_label
+ sql << alter_property_graph_element_table_sql(op) <<
+ " DROP LABEL " << quote_identifier(op[:label])
+ when :add_properties
+ sql << alter_property_graph_element_table_sql(op) <<
+ " ALTER LABEL " << quote_identifier(op[:label]) << " ADD PROPERTIES " <<
+ literal(op[:properties])
+ when :drop_properties
+ sql << alter_property_graph_element_table_sql(op) <<
+ " ALTER LABEL " << quote_identifier(op[:label]) << " DROP PROPERTIES " <<
+ literal(op[:properties])
+ else # when :set_owner
+ sql << "OWNER TO " << literal(op[:owner])
+ end
+
+ case op_type
+ when :drop_vertex_tables, :drop_edge_tables, :drop_label, :drop_properties
+ sql << " CASCADE" if op[:cascade]
+ end
+
+ sql
+ end
+
+ # SQL fragment for the ALTER PROPERTY GRAPH ALTER {VERTEX|EDGE} TABLE prefix
+ def alter_property_graph_element_table_sql(op)
+ "ALTER #{op[:kind] == :vertex ? 'VERTEX' : 'EDGE'} TABLE #{quote_identifier(op[:name])}"
+ end
+
@@ -1599,0 +2450,81 @@
+ # SQL statement for creating a property graph.
+ def create_property_graph_sql(name, data, opts=OPTS)
+ sql = String.new
+ sql << "CREATE "
+ sql << "TEMPORARY " if opts[:temp]
+ sql << "PROPERTY GRAPH "
+ sql << quote_schema_table(name)
+
+ unless data.vertices.empty?
+ sql << " VERTEX TABLES ("
+ sql << data.vertices.map do |vertex|
+ create_property_graph_table_sql(vertex) <<
+ create_property_graph_labels_sql(vertex.labels)
+ end.join(', ')
+ sql << ")"
+ end
+
+ unless data.edges.empty?
+ sql << " EDGE TABLES ("
+ sql << data.edges.map do |edge|
+ create_property_graph_table_sql(edge) <<
+ " SOURCE " << create_property_graph_edge_side_sql(edge.source) <<
+ " DESTINATION " << create_property_graph_edge_side_sql(edge.destination) <<
+ create_property_graph_labels_sql(edge.labels)
+ end.join(', ')
+ sql << ")"
+ end
+
+ sql
+ end
+
+ # SQL fragment for the SOURCE or DESTINATION clause of an edge in a property graph.
+ def create_property_graph_edge_side_sql(side)
+ sql = String.new
+ if side.key
+ sql << "KEY " << literal(side.key) << " REFERENCES "
+ end
+ sql << quote_identifier(side.name)
+ if side.references
+ sql << " " << literal(side.references)
+ end
+ sql
+ end
+
+ # SQL fragment for the table name and KEY clause used for vertices and edges in
+ # a property graph.
+ def create_property_graph_table_sql(element)
+ sql = String.new
+ sql << literal(element.name)
+
+ if key = element.key
+ sql << " KEY " << literal(key)
+ end
+
+ sql
+ end
+
+ # SQL fragment for the LABEL/PROPERTIES clauses used for vertices and
+ # edges in a property graph.
+ def create_property_graph_labels_sql(labels)
+ labels.map do |name, properties|
+ sql = String.new
+ sql << " LABEL " << quote_identifier(name) if name
+ sql << create_property_graph_properties_clause_sql(properties)
+ sql
+ end.join
+ end
+
+ # SQL fragment for the NO PROPERTIES, PROPERTIES ALL COLUMNS, or
+ # PROPERTIES (...) clause for a property graph element or label.
+ def create_property_graph_properties_clause_sql(properties)
+ case properties
+ when nil, :all
+ " PROPERTIES ALL COLUMNS"
+ when false, :none, [].freeze
+ " NO PROPERTIES"
+ else
+ " PROPERTIES #{literal(properties)}"
+ end
+ end
+
@@ -1712,0 +2644,5 @@
+ # SQL for dropping a property graph from the database.
+ def drop_property_graph_sql(name, opts=OPTS)
+ "DROP PROPERTY GRAPH#{' IF EXISTS' if opts[:if_exists]} #{literal(name)}#{' CASCADE' if opts[:cascade]}"
+ end
+
@@ -2135,0 +3072,20 @@
+ # Set FOR PORTION OF clause for UPDATE and DELETE statements.
+ # The first argument is the range or multirange column. If two arguments
+ # are provided, the second argument is an expression with the same
+ # database type as the first argument. If three arguments are provided,
+ # the second specifies the inclusive start of the portion to update and the third
+ # specifies the exclusive end of portion to update. When using the three argument
+ # form, nil can be provided as the second or third argument to have the start or
+ # end of the portion be unbounded. Supported on PostgreSQL 19+.
+ # Example:
+ #
+ # DB[:t].for_portion_of(:rc, Sequel.function(:int4range, 1, 2)).update(c: 3)
+ # # UPDATE t FOR PORTION OF rc (int4range(1, 2)) SET c = 3
+ #
+ # DB[:t].for_portion_of(:rc, 1, 2).update(c: 3)
+ # # UPDATE t FOR PORTION OF rc FROM 1 TO 2 SET c = 3
+ def for_portion_of(column, range, to=(arg_not_given=true))
+ range = [range, to].freeze unless arg_not_given
+ clone(:for_portion_of => [column, range].freeze)
+ end
+
@@ -2638 +3594,2 @@
- # Only include the primary table in the main delete clause
+ # Only include the primary table in the main delete clause.
+ # Support FOR PORTION OF.
@@ -2641 +3598 @@
- source_list_append(sql, @opts[:from][0..0])
+ table_for_portion_of_sql_append(sql)
@@ -2725,0 +3683,26 @@
+ # Add FOR PORTION OF SQL if the dataset uses it.
+ def table_for_portion_of_sql_append(sql)
+ fpo_column, fpo_range = @opts[:for_portion_of]
+ if fpo_column
+ table, aliaz = split_alias(@opts[:from].first)
+ source_list_append(sql, [table])
+ sql << ' FOR PORTION OF '
+ literal_append(sql, fpo_column)
+
+ if fpo_range.is_a?(Array)
+ fpo_start, fpo_end = fpo_range
+ sql << ' FROM '
+ literal_append(sql, fpo_start)
+ sql << ' TO '
+ literal_append(sql, fpo_end)
+ else
+ sql << ' ('
+ literal_append(sql, fpo_range)
+ sql << ')'
+ end
+ as_sql_append(sql, aliaz) if aliaz
+ else
+ source_list_append(sql, @opts[:from][0..0])
+ end
+ end
+
@@ -3015 +3998 @@
- # Only include the primary table in the main update clause
+ # Support FOR PORTION OF.
@@ -3018 +4001 @@
- source_list_append(sql, @opts[:from][0..0])
+ table_for_portion_of_sql_append(sql)
lib/sequel/adapters/shared/sqlite.rb
--- /tmp/d20260805-1551-hhqd62/sequel-5.105.0/lib/sequel/adapters/shared/sqlite.rb 2026-08-05 02:33:32.033018220 +0000
+++ /tmp/d20260805-1551-hhqd62/sequel-5.107.0/lib/sequel/adapters/shared/sqlite.rb 2026-08-05 02:33:32.084018255 +0000
@@ -680,3 +680,3 @@
- # Return an array of strings specifying a query explanation for a SELECT of the
- # current dataset. Currently, the options are ignored, but it accepts options
- # to be compatible with other adapters.
+ # Return a string specifying a query explanation for a SELECT of the
+ # current dataset. Options:
+ # :query_plan :: Use EXPLAIN QUERY PLAN instead of EXPLAIN if true.
@@ -687 +687,2 @@
- ds = db.send(:metadata_dataset).clone(:sql=>"EXPLAIN #{select_sql}".freeze)
+ keyword = (opts && opts[:query_plan]) ? "EXPLAIN QUERY PLAN" : "EXPLAIN"
+ ds = db.send(:metadata_dataset).clone(:sql=>"#{keyword} #{select_sql}".freeze)
lib/sequel/database/schema_generator.rb
--- /tmp/d20260805-1551-hhqd62/sequel-5.105.0/lib/sequel/database/schema_generator.rb 2026-08-05 02:33:32.038018223 +0000
+++ /tmp/d20260805-1551-hhqd62/sequel-5.107.0/lib/sequel/database/schema_generator.rb 2026-08-05 02:33:32.088018258 +0000
@@ -20 +20,3 @@
- break loc unless loc.path == __FILE__
+ # Skip core library methods implemented in Ruby
+ path = loc.path
+ break loc unless path == __FILE__ || (path && path.start_with?('<internal:'))
lib/sequel/dataset/sql.rb
--- /tmp/d20260805-1551-hhqd62/sequel-5.105.0/lib/sequel/dataset/sql.rb 2026-08-05 02:33:32.041018225 +0000
+++ /tmp/d20260805-1551-hhqd62/sequel-5.107.0/lib/sequel/dataset/sql.rb 2026-08-05 02:33:32.091018260 +0000
@@ -564,0 +565 @@
+ sql << ' IGNORE NULLS' if window.opts[:ignore_nulls]
lib/sequel/sql.rb
--- /tmp/d20260805-1551-hhqd62/sequel-5.105.0/lib/sequel/sql.rb 2026-08-05 02:33:32.076018249 +0000
+++ /tmp/d20260805-1551-hhqd62/sequel-5.107.0/lib/sequel/sql.rb 2026-08-05 02:33:32.129018286 +0000
@@ -1325,0 +1326 @@
+ ALL = Constant.new(:ALL)
@@ -1994,0 +1996,2 @@
+ # :ignore_nulls :: Can be set to :ignore for IGNORE NULLS (supported on PostgreSQL 19+ for
+ # a subset of default window functions)
lib/sequel/version.rb
--- /tmp/d20260805-1551-hhqd62/sequel-5.105.0/lib/sequel/version.rb 2026-08-05 02:33:32.076018249 +0000
+++ /tmp/d20260805-1551-hhqd62/sequel-5.107.0/lib/sequel/version.rb 2026-08-05 02:33:32.129018286 +0000
@@ -9 +9 @@
- MINOR = 105
+ MINOR = 107 |
Contributor
Contributor
gem compare sequel 5.105.0 5.107.0Compared versions: ["5.105.0", "5.107.0"]
DIFFERENT rubygems_version:
5.105.0: 4.0.10
5.107.0: 4.0.16
DIFFERENT version:
5.105.0: 5.105.0
5.107.0: 5.107.0
DIFFERENT files:
5.105.0->5.107.0:
* Changed:
lib/sequel/adapters/shared/postgres.rb +987/-4
lib/sequel/adapters/shared/sqlite.rb +5/-4
lib/sequel/database/schema_generator.rb +3/-1
lib/sequel/dataset/sql.rb +1/-0
lib/sequel/sql.rb +3/-0
lib/sequel/version.rb +1/-1 |
Contributor
Contributor
gem compare sequel 5.105.0 5.107.0Compared versions: ["5.105.0", "5.107.0"]
DIFFERENT rubygems_version:
5.105.0: 4.0.10
5.107.0: 4.0.16
DIFFERENT version:
5.105.0: 5.105.0
5.107.0: 5.107.0
DIFFERENT files:
5.105.0->5.107.0:
* Changed:
lib/sequel/adapters/shared/postgres.rb +987/-4
lib/sequel/adapters/shared/sqlite.rb +5/-4
lib/sequel/database/schema_generator.rb +3/-1
lib/sequel/dataset/sql.rb +1/-0
lib/sequel/sql.rb +3/-0
lib/sequel/version.rb +1/-1 |
Contributor
gem compare --diff sequel 5.105.0 5.107.0Compared versions: ["5.105.0", "5.107.0"]
DIFFERENT files:
5.105.0->5.107.0:
* Changed:
lib/sequel/adapters/shared/postgres.rb
--- /tmp/d20260805-1596-i6n9tc/sequel-5.105.0/lib/sequel/adapters/shared/postgres.rb 2026-08-05 02:33:52.470710648 +0000
+++ /tmp/d20260805-1596-i6n9tc/sequel-5.107.0/lib/sequel/adapters/shared/postgres.rb 2026-08-05 02:33:52.518711308 +0000
@@ -258,0 +259,558 @@
+ module PropertyGraph
+ # Base class for all Generator DSL classes. This uses a design where
+ # The DSL class is only used for the evaluation of the block, and new
+ # returns a frozen struct.
+ class Generator
+ # Instead of returning the Generator instance, return a frozen struct
+ # with data from the generator. This prevents accidentally calling the
+ # generator methods, and makes it possible for the generator class and
+ # result class to use the same method name in two different ways, with
+ # the generator setting data and the frozen struct method returning it.
+ # The frozen struct classes use the constant Data under each generator
+ # subclass.
+ def self.new(*args, &block)
+ super(*args, &block).data
+ end
+
+ # Base class for Vertex and Edge.
+ class Element < self
+ Data = Struct.new(:name, :key, :labels)
+
+ # +name+ specifies the name of the vertex or edge. It can be an
+ # SQL::AliasedExpression to use an alias. Options:
+ # :properties :: Specifies fixed properties for the vertex or edge.
+ # If this is given, you cannot use the label method
+ # inside the block.
+ def initialize(name, opts=OPTS, &block)
+ @name = name
+ @labels = []
+ if opts.key?(:properties)
+ @labels << [nil, opts[:properties]].freeze
+ @labels.freeze
+ end
+ instance_exec(&block) if block
+ @labels.freeze
+ freeze
+ end
+
+ def data
+ Data.new(@name, @key, @labels).freeze
+ end
+
+ # Set the column(s) to use for the KEY clause, which are the columns
+ # that uniquely identify rows in the table:
+ #
+ # key(:id)
+ # # KEY (id)
+ #
+ # key([:id1, :id2])
+ # # KEY (id1, id2)
+ def key(columns)
+ @key = Array(columns)
+ end
+
+ # Add a label and properties for the label for this vertex/edge.
+ # A vertex or edge can have multiple labels with separate properties,
+ # if it wasn't created with fixed properties. The +name+ argument
+ # specifies the label name. The +properties+ argument specifies the
+ # properties:
+ # nil, :all :: PROPERTIES ALL COLUMNS
+ # false, :none, [] :: NO PROPERTIES
+ # Array :: Array of specific properties. Each element should be a Symbol,
+ # SQL::Identifier, or SQL::AliasedExpression.
+ #
+ # label(:label_name)
+ # # LABEL label_name PROPERTIES ALL COLUMNS
+ #
+ # label(:label_name, [])
+ # # LABEL label_name NO PROPERTIES
+ #
+ # label(:label_name, [:c, Sequel[:b].as(:d)], Sequel[:e])
+ # # LABEL label_name PROPERTIES (c, b AS d, e)
+ def label(name, properties=:all)
+ if @labels.frozen?
+ raise Error, "cannot specify label for property graph vertex or edge with fixed properties"
+ end
+ @labels << [name, properties].freeze
+ nil
+ end
+ end
+
+ # Vertex is used for the block passed to Create#vertex, used to configure
+ # vertices in the property graph. It doesn't have any additional behavior
+ # compared to the Element class, so this is an alias instead of a subclass.
+ Vertex = Element
+
+ # Target is used for the block passed to Edge#source and Edge#destination,
+ # used to configure the source and destination of property graph edges.
+ class Target < self
+ Data = Struct.new(:name, :key, :references)
+
+ # +name+ specifies the name of the source or destination.
+ def initialize(name, &block)
+ @name = name
+ @key = nil
+ @references = nil
+ instance_exec(&block) if block
+ freeze
+ end
+
+ def data
+ Data.new(@name, @key, @references).freeze
+ end
+
+ # Set the column(s) to use for the KEY clause, which are the columns
+ # in the edge table that reference columns in the source or destination.
+ # Should be combined with #references to specify the columns being
+ # referenced.
+ #
+ # key(:vertex_id)
+ # # KEY (vertex_id)
+ #
+ # key([:vertex_id1, :vertex_id2])
+ # # KEY (vertex_id1, vertex_id2)
+ def key(keys)
+ @key = Array(keys)
+ end
+
+ # Set the column(s) to use for the REFERENCES clause, which are the columns
+ # in the source or destination table that are referenced by the edge table.
+ # Should be combined with #key to specify the columns doing the referencing.
+ #
+ # references(:id)
+ # # REFERENCES (id)
+ #
+ # references([:id1, :id2])
+ # # REFERENCES (id1, id2)
+ def references(refs)
+ @references = Array(refs)
+ end
+ end
+
+ # Edge is used for block passed to Create#edge, used to configure edges
+ # in the property graph.
+ class Edge < Element
+ Data = Struct.new(:name, :key, :labels, :source, :destination)
+
+ # In addition to inherited behavior, raises an error if a block
+ # is not passed or source or destination is not called in the block.
+ def initialize(name, opts=OPTS, &block)
+ super
+
+ unless @source && @destination
+ raise Error, "source and/or destination not defined for property graph edge"
+ end
+ end
+
+ def data
+ Data.new(@name, @key, @labels, @source, @destination).freeze
+ end
+
+ # Specify the source for the edge, with block evaluted by Target.
+ def source(name, &block)
+ raise Error, "cannot specify multiple sources for a property graph edge" if @source
+ @source = Target.new(name, &block)
+ end
+
+ # Specify the destination for the edge, with block evaluted by Target.
+ def destination(name, &block)
+ raise Error, "cannot specify multiple destinations for a property graph edge" if @destination
+ @destination = Target.new(name, &block)
+ end
+ end
+
+ # Create is used to evaluate the block given to DatabaseMethods#create_property_graph,
+ # used to specify the vertices and edges in the property graph.
+ class Create < self
+ Data = Struct.new(:vertices, :edges)
+
+ def initialize(&block)
+ @vertices = []
+ @edges = []
+ instance_exec(&block)
+ @vertices.freeze
+ @edges.freeze
+ freeze
+ end
+
+ def data
+ Data.new(@vertices, @edges).freeze
+ end
+
+ # Adds a vertex to the property graph, with the block evaluted by Vertex.
+ def vertex(name, opts=OPTS, &block)
+ @vertices << Vertex.new(name, opts, &block)
+ end
+
+ # Adds an edge to the property graph, with the block evaluted by Edge.
+ def edge(name, opts=OPTS, &block)
+ @edges << Edge.new(name, opts, &block)
+ end
+ end
+
+ # AlterElement is used to evaluate the block passed to
+ # Alter#alter_vertex_table and Alter#alter_edge_table.
+ class AlterElement < self
+ # +kind+ is +:vertex+ or +:edge+. +name+ is the alias of the
+ # vertex or edge table to alter.
+ def initialize(kind, name, &block)
+ @kind = kind
+ @name = name
+ @labels = []
+ @operations = []
+ instance_exec(&block)
+
+ # All labels added via #add_label are combined into a single
+ # ADD LABEL operation, as PostgreSQL supports adding multiple
+ # labels in a single ALTER ... ADD LABEL statement.
+ unless @labels.empty?
+ @operations << {:op=>:add_label, :kind=>kind, :name=>name, :labels=>@labels.freeze}
+ end
+
+ @operations.each(&:freeze)
+ @operations.freeze
+ freeze
+ end
+
+ def data
+ @operations
+ end
+
+ # Add a label (and optional properties) to the vertex/edge table.
+ # Takes the same arguments as Element#label. Can be called multiple
+ # times to add multiple labels.
+ #
+ # add_label(:l)
+ # # ADD LABEL l PROPERTIES ALL COLUMNS
+ def add_label(name, properties=:all)
+ @labels << [name, properties].freeze
+ nil
+ end
+
+ # Remove a label from the vertex/edge table. Options:
+ # :cascade :: Use CASCADE to drop dependent objects.
+ #
+ # drop_label(:l)
+ # # DROP LABEL l
+ def drop_label(name, opts=OPTS)
+ @operations << {:op=>:drop_label, :kind=>@kind, :name=>@name, :label=>name, :cascade=>opts[:cascade]}
+ nil
+ end
+
+ # Add properties to an existing label on the vertex/edge table.
+ # +properties+ is an expression, or array of expressions, the same
+ # as the explicit array form of the +properties+ argument to
+ # Element#label.
+ #
+ # add_properties(:l, [:c1, Sequel[:c2].as(:c3)])
+ # # ALTER LABEL l ADD PROPERTIES (c1, c2 AS c3)
+ def add_properties(label, properties)
+ @operations << {:op=>:add_properties, :kind=>@kind, :name=>@name, :label=>label, :properties=>Array(properties)}
+ nil
+ end
+
+ # Remove properties from an existing label on the vertex/edge table.
+ # +properties+ is a column name, or array of column names. Options:
+ # :cascade :: Use CASCADE to drop dependent objects.
+ #
+ # drop_properties(:l, [:c1])
+ # # ALTER LABEL l DROP PROPERTIES (c1)
+ def drop_properties(label, properties, opts=OPTS)
+ @operations << {:op=>:drop_properties, :kind=>@kind, :name=>@name, :label=>label, :properties=>Array(properties), :cascade=>opts[:cascade]}
+ nil
+ end
+ end
+
+ # Alter is used to evaluate the block given to DatabaseMethods#alter_property_graph,
+ # used to specify changes to an existing property graph.
+ class Alter < self
+ def initialize(&block)
+ @operations = []
+ instance_exec(&block)
+
+ @operations.each do |op|
+ case op[:op]
+ when :add_vertex_tables, :add_edge_tables
+ op[:tables].freeze
+ end
+ op.freeze
+ end
+ @operations.freeze
+ freeze
+ end
+
+ def data
+ @operations
+ end
+
+ # Add a vertex to the property graph, with the block used to configure the
+ # vertex.
+ #
+ # alter_property_graph.add_vertex(:v)
+ # # ADD VERTEX TABLES (v)
+ def add_vertex(name, opts=OPTS, &block)
+ add_tables_operation(:add_vertex_tables) << Vertex.new(name, opts, &block)
+ nil
+ end
+
+ # Add an edge to the property graph, with the block used to configure the edge.
+ #
+ # alter_property_graph.add_edge(:e){source :v1; destination :v2}
+ # # ADD EDGE TABLES (e SOURCE v1 DESTINATION v2)
+ def add_edge(name, opts=OPTS, &block)
+ add_tables_operation(:add_edge_tables) << Edge.new(name, opts, &block)
+ nil
+ end
+
+ # Remove vertex tables (referenced by their aliases) from the
+ # property graph. +aliases+ can be a single alias or an array.
+ # Options:
+ # :cascade :: Use CASCADE instead of the default RESTRICT.
+ #
+ # alter_property_graph.drop_vertex_tables([:v1, :v2])
+ # # DROP VERTEX TABLES (v1, v2)
+ def drop_vertex_tables(aliases, opts=OPTS)
+ @operations << {:op=>:drop_vertex_tables, :aliases=>Array(aliases), :cascade=>opts[:cascade]}
+ nil
+ end
+
+ # Remove edge tables (referenced by their aliases) from the property
+ # graph. See #drop_vertex_tables.
+ #
+ # alter_property_graph.drop_edge_tables([:e1, :e2])
+ # # DROP EDGE TABLES (e1, e2)
+ def drop_edge_tables(aliases, opts=OPTS)
+ @operations << {:op=>:drop_edge_tables, :aliases=>Array(aliases), :cascade=>opts[:cascade]}
+ nil
+ end
+
+ # Modify an existing vertex table (referenced by its alias).
+ #
+ # alter_property_graph.alter_vertex_table(:v){add_label :l}
+ # # ALTER VERTEX TABLE v ADD LABEL l PROPERTIES ALL COLUMNS
+ def alter_vertex_table(name, &block)
+ @operations.concat(AlterElement.new(:vertex, name, &block))
+ nil
+ end
+
+ # Modify an existing edge table (referenced by its alias).
+ #
+ # alter_property_graph.alter_edge_table(:e, properties: :none){drop_label :l}
+ # # ALTER VERTEX TABLE e DROP LABEL l
+ def alter_edge_table(name, &block)
+ @operations.concat(AlterElement.new(:edge, name, &block))
+ nil
+ end
+
+ # Change the owner of the property graph. +new_owner+ is usually a
+ # Symbol or SQL::Identifier for the role name, but can be
+ # <tt>Sequel.lit('CURRENT_USER')</tt> or
+ # <tt>Sequel.lit('SESSION_USER')</tt>.
+ #
+ # alter_property_graph.owner_to(:new_owner)
+ # # OWNER TO new_owner
+ def set_owner(new_owner)
+ @operations << {:op=>:set_owner, :owner=>new_owner}
+ nil
+ end
+
+ private
+
+ # Internals of add_vertex and add_edge.
+ def add_tables_operation(op_name)
+ unless op = @operations.find{|o| o[:op] == op_name}
+ @operations << (op = {:op=>op_name, :tables=>[]})
+ end
+ op[:tables]
+ end
+ end
+ end
+
+ # Represents a GRAPH_TABLE expression, used to query a property graph
+ # via graph pattern matching. This is used in place of a table name
+ # expression or dataset in a SELECT query. These are created by calling
+ # #graph_table on the related Database object.
+ #
+ # Table uses a method chaining design, similar to Dataset, where methods
+ # return modified frozen copies of the object.
+ class Table
+ include SQL::AliasMethods
+
+ # Internal struct for a single element (vertex or edge) in the graph pattern:
+ # +type+ :: Either :vertex or :edge.
+ # +marker+ :: Connector string to use for the element (empty for initial vertex).
+ # +label+ :: Label restriction symbol or SQL::Identifier for the element, if any.
+ # Can be an array or set to match multiple labels.
+ # +var+ :: Graph pattern variable symbol for the element, if any.
+ # +where+ :: WHERE condition for the element, if any.
+ Element = Struct.new(:type, :marker, :label, :var, :where) do
+ # Method used to create elements, used instead of new
+ # to ensure that the returned elements are frozen.
+ def self.create(type, marker, label, opts)
+ case label
+ when Array, Set
+ label = label.dup.freeze unless label.frozen?
+ end
+
+ case where = opts[:where]
+ when Hash, Array
+ where = SQL::BooleanExpression.from_value_pairs(where)
+ end
+
+ new(type, marker, label, opts[:var], where).freeze
+ end
+
+ private_class_method :new
+ end
+ private_constant :Element
+
+ # The name of the property graph the table is querying.
+ attr_reader :name
+
+ # A frozen array of Element instances, representing the vertices and
+ # edges in the graph pattern.
+ attr_reader :elements
+
+ # A frozen array of the columns used in the COLUMNS clause (aliased
+ # as columns_used, as #columns is used to modify the columns).
+ attr_reader :columns
+ alias columns_used columns
+
+ # Create a new Table with the given +graph_name+, with +initial_vertex_label+
+ # and +initial_vertex_opts+ being used to create the initial vertex.
+ # See Table#link for which options are supported for the initial vertex.
+ def self.create(graph_name, initial_vertex_label, initial_vertex_opts)
+ vertex = Element.create(:vertex, "", initial_vertex_label, initial_vertex_opts)
+ new(graph_name, [vertex].freeze, [].freeze)
+ end
+
+ def initialize(name, elements, columns)
+ @name = name
+ @elements = elements
+ @columns = columns
+ freeze
+ end
+
+ # Return a modified copy with an element added using a bidirectional link
+ # (<tt>-</tt> in the graph pattern).
+ # +label+ specifies the label restriction for the element. This can be
+ # nil for no label restriction, or an array or set to restrict to the
+ # given labels.
+ #
+ # Options supported:
+ # +:var+ :: Specifies a graph pattern variable name for the element,
+ # usable in the WHERE or COLUMNS clauses.
+ # +:vertex+ :: Specifies that the element being linked to is a vertex.
+ # This allows for direct vertex<->vertex linking, instead of
+ # the default vertex<->edge<->vertex linking.
+ # +:where+ :: An expression to use for the WHERE clause for the element.
+ #
+ # DB.graph_table(:gn, :v).link(:e)
+ # # GRAPH_TABLE (gn MATCH (IS v)-[IS e])
+ def link(label, opts=OPTS)
+ append_element('-', label, opts)
+ end
+
+ # Similar to #link, but uses a directed link from the previous element
+ # to the new element (<tt>-></tt> in the graph pattern). Accepts same
+ # arguments and options as #link.
+ #
+ # DB.graph_table(:gn, :v).to(:e)
+ # # GRAPH_TABLE (gn MATCH (IS v)->[IS e])
+ def to(label, opts=OPTS)
+ append_element('->', label, opts)
+ end
+
+ # Similar to #link, but uses a directed link from the new element
+ # to the previous element (<tt><-</tt> in the graph pattern). Accepts
+ # same arguments and options as #link.
+ #
+ # DB.graph_table(:gn, :v).from(:e)
+ # # GRAPH_TABLE (gn MATCH (IS v)<-[IS e])
+ def from(label, opts=OPTS)
+ append_element('<-', label, opts)
+ end
+
+ # Return a modifies copy that uses the given columns. A graph table
+ # must have a least one column set before it is used in a query.
+ #
+ # DB.graph_table(:gn, :v).columns(:a, Sequel[:b].as(:c))
+ # # GRAPH_TABLE (gn MATCH (IS v) COLUMNS (a, b AS c))
+ def columns(*cols)
+ self.class.new(@name, @elements, cols.freeze)
+ end
+
+ # Return a modified copy that adds the given columns to the existing
+ # list of columns for the graph table.
+ def add_columns(*cols)
+ columns(*@columns, *cols)
+ end
+
+ # Append the SQL for the GRAPH_TABLE expression to the given SQL string.
+ # Requires graph table have at least one column set.
+ def sql_literal_append(ds, sql)
+ if @columns.empty?
+ raise Error, "cannot use graph_table in a query if it does not return any columns"
+ end
+ if @elements.last.type == :edge
+ raise Error, "cannot use graph_table in a query if the last element is an edge"
+ end
+
+ sql << "GRAPH_TABLE ("
+ ds.literal_append(sql, @name)
+ sql << " MATCH "
+
+ @elements.each do |element|
+ marker = element.marker
+ var = element.var
+ label = element.label
+ where = element.where
+ vertex = element.type == :vertex
+
+ sql << marker
+ sql << (vertex ? '(' : '[')
+
+ ds.literal_append(sql, var) if var
+ if label
+ sql << (var ? " IS " : "IS ")
+ if label.is_a?(Array)
+ label_sep = ""
+ label.each do |l|
+ sql << label_sep
+ label_sep = "|" if label_sep.empty?
+ ds.literal_append(sql, l)
+ end
+ else
+ ds.literal_append(sql, label)
+ end
+ end
+
+ if where
+ sql << ((var || label) ? " WHERE " : "WHERE ")
+ ds.literal_append(sql, where)
+ end
+
+ sql << (vertex ? ')' : ']')
+ end
+
+ sql << " COLUMNS "
+ ds.literal_append(sql, @columns)
+ sql << ")"
+ end
+
+ private
+
+ # Internals of #link, #to, and #from.
+ def append_element(marker, label, opts)
+ node_type = if opts[:vertex]
+ :vertex
+ else
+ @elements.last.type == :vertex ? :edge : :vertex
+ end
+
+ element = Element.create(node_type, marker, label, opts)
+ self.class.new(@name, (@elements.dup << element).freeze, @columns)
+ end
+ end
+ end
+
@@ -340,0 +899,58 @@
+ # Alter the property graph with the given +name+, supported on PostgreSQL 19+.
+ # The block uses a DSL, evaluated by PropertyGraph::Generator::Alter. Example:
+ #
+ # DB.alter_property_graph(:my_graph) do
+ # # PropertyGraph::Generator::Alter
+ # add_vertex :companies2
+ # # ALTER PROPERTY GRAPH "my_graph" ADD VERTEX TABLES ("companies2")
+ #
+ # add_edge :works_at2 do
+ # # PropertyGraph::Generator::Edge
+ # source :people
+ # destination :companies2
+ # end
+ # # ALTER PROPERTY GRAPH "my_graph" ADD EDGE TABLES
+ # # ("works_at2" SOURCE "people" DESTINATION "companies2")
+ #
+ # drop_vertex_tables [:p2], cascade: true
+ # # ALTER PROPERTY GRAPH "my_graph" DROP VERTEX TABLES ("p2") CASCADE
+ #
+ # drop_edge_tables :e2
+ # # ALTER PROPERTY GRAPH "my_graph" DROP EDGE TABLES ("e2")
+ #
+ # alter_vertex_table :companies do
+ # # PropertyGraph::Generator::AlterElement
+ # add_label :public_company, [:name, :symbol]
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER VERTEX TABLE "companies"
+ # # ADD LABEL "public_company" PROPERTIES ("name", "symbol")
+ #
+ # drop_label :private_company
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER VERTEX TABLE "companies"
+ # # DROP LABEL "private_company"
+ #
+ # add_properties :company, :revenue
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER VERTEX TABLE "companies"
+ # # ALTER LABEL "company" ADD PROPERTIES ("revenue")
+ #
+ # drop_properties :company, :internal_id, cascade: true
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER VERTEX TABLE "companies"
+ # # ALTER LABEL "company" DROP PROPERTIES ("internal_id") CASCADE
+ # end
+ #
+ # alter_edge_table :works_at do
+ # # PropertyGraph::Generator::AlterElement
+ # add_label :employment
+ # end
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER EDGE TABLE "works_at"
+ # # ADD LABEL "employment" PROPERTIES ALL COLUMNS
+ #
+ # owner_to :new_owner
+ # # ALTER PROPERTY GRAPH "my_graph" OWNER TO "new_owner"
+ # end
+ def alter_property_graph(name, &block)
+ PropertyGraph::Generator::Alter.new(&block).each do |op|
+ execute_ddl(alter_property_graph_op_sql(name, op).freeze)
+ end
+ nil
+ end
+
@@ -469,0 +1086,61 @@
+ # Create a property graph in the database, supported on PostgreSQL 19+.
+ #
+ # Arguments:
+ # name :: Name of the property graph
+ # opts :: options hash:
+ # :temp :: Create the property graph as a temporary property graph.
+ #
+ # The block uses a DSL, with classes under PropertyGraph::Generator:
+ #
+ # DB.create_property_graph(:my_graph) do
+ # # PropertyGraph::Generator::Create
+ # vertex :people
+ #
+ # vertex Sequel.as(:people, :p), properties: []
+ #
+ # vertex Sequel.as(:companies, :c) do
+ # # PropertyGraph::Generator::Vertex
+ # key :id
+ # label :company
+ # label :c, [:name, (Sequel[:revenue] / 1000).as(:revenue_thousands)]
+ # end
+ #
+ # edge :works_at do
+ # # PropertyGraph::Generator::Edge
+ # source :people
+ # destination :c
+ # end
+ #
+ # edge Sequel.as(:employment, :e) do
+ # source :people do
+ # # PropertyGraph::Generator::Target
+ # key :person_id
+ # references :id
+ # end
+ # destination :c do
+ # # PropertyGraph::Generator::Target
+ # key :company_id
+ # references :id
+ # end
+ # label :employment
+ # end
+ # end
+ # # CREATE PROPERTY GRAPH "my_graph"
+ # # VERTEX TABLES (
+ # # "people",
+ # # "people" AS "p" NO PROPERTIES,
+ # # "companies" AS "c" KEY ("id")
+ # # LABEL "company" PROPERTIES ALL COLUMNS
+ # # LABEL "c" PROPERTIES ("name", ("revenue" / 1000) AS "revenue_thousands"))
+ # # EDGE TABLES (
+ # # "works_at"
+ # # SOURCE "people"
+ # # DESTINATION "c",
+ # # "employment" AS "e"
+ # # SOURCE KEY ("person_id") REFERENCES "people" ("id")
+ # # DESTINATION KEY ("company_id") REFERENCES "c" ("id")
+ # # LABEL "employment" PROPERTIES ALL COLUMNS)
+ def create_property_graph(name, opts=OPTS, &block)
+ execute_ddl(create_property_graph_sql(name, PropertyGraph::Generator::Create.new(&block), opts))
+ end
+
@@ -566,0 +1244,9 @@
+ # Drops a property graph from the database. Arguments:
+ # name :: name of the property graph to drop
+ # opts :: options hash:
+ # :cascade :: Drop other objects depending on this property_graph.
+ # :if_exists :: Don't raise an error if the property graph doesn't exist.
+ def drop_property_graph(name, opts=OPTS)
+ self << drop_property_graph_sql(name, opts).freeze
+ end
+
@@ -653,0 +1340,76 @@
+ # Return a PropertyGraph::Table instance for a property graph search
+ # (a GRAPH_TABLE clause for a SELECT query). Supported on PostgreSQL 19+.
+ #
+ # Arguments:
+ # +property_graph_name+ :: The property graph to query
+ # +initial_vertex_label+ :: The label restriction for the initial vertex for the
+ # graph pattern (can be nil for no label, or an array
+ # or set for restricting to one of multiple labels).
+ # +initial_vertex_opts+ :: The options for the initial vertex, see
+ # PropertyGraph::Table#link for available options.
+ #
+ # The returned instance should be further modified by calling methods on it,
+ # using a similar approach to how datasets work, where the methods return a
+ # modified copy of the receiver. The available methods:
+ #
+ # link :: Add a bidirectional link to a new element (vertex or edge)
+ # to :: Add a directional link from the last element to the new element
+ # from :: Add a direciton link from the new element to last element
+ # columns :: Replace the columns the graph table returns
+ # add_columns :: Append to the columns the graph table returns.
+ #
+ # See PropertyGraph::Table for the details of these methods and the arguments
+ # and options they support. Note that for a graph table to be usable in a query,
+ # it must return at least one column, and the last element in the graph pattern
+ # must be a vertex.
+ #
+ # gt = DB.graph_table(:pgn, :iv)
+ # # Not yet usable, does not return any columns
+ #
+ # # Set columns for graph table
+ # gt = gt.columns(:c, Sequel[1].as(:d))
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv") COLUMNS ("c", 1 AS "d"))
+ #
+ # # Adds directional link to edge, since last (initial) element was a vertex
+ # gt = gt.link(:e1)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"] COLUMNS ("c", 1 AS "d"))
+ #
+ # # Adds directional link from edge to vertex, since last element was an edge
+ # gt = gt.to(:v2)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2") COLUMNS ("c", 1 AS "d"))
+ #
+ # # Adds bidirection link from vertex to vertex (overriding the default)
+ # gt = gt.link(:v3, vertex: true)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3") COLUMNS ("c", 1 AS "d"))
+ #
+ # # Adds directional link from new edge to last vertex, since last element was an vertex.
+ # # Sets graph pattern variable name and uses it in a WHERE clause for the added element.
+ # gt = gt.from(:e2, var: :a2, where: {Sequel[:a2][:c] => 1})
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3")
+ # # <-["a2" IS "e2" WHERE ("a2"."c" = 1)] COLUMNS ("c", 1 AS "d"))
+ #
+ # # Can use nil as a label for no label restriction, both with and without a variable name
+ # gt = gt.to(nil).to(nil, var: :a3)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3")
+ # # <-["a2" IS "e2" WHERE ("a2"."c" = 1)]->[]->("a3") COLUMNS ("c", 1 AS "d"))
+ #
+ # # Can restrict to a one of a set of labels
+ # gt = gt.from([:x, :y], var: :a6)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3")
+ # # <-["a2" IS "e2" WHERE ("a2"."c" = 1)]->[]->("a3")->["a6" IS "x"|"y"] COLUMNS ("c", 1 AS "d"))
+ #
+ # # Add column(s) to the graph table
+ # gt = gt.add_columns(:y)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3")
+ # # <-["a2" IS "e2" WHERE ("a2"."c" = 1)]->[]->("a3")->["a6" IS "x"|"y"]
+ # # COLUMNS ("c", 1 AS "d", "y"))
+ #
+ # DB.from(gt)
+ # # SELECT * FROM GRAPH_TABLE (...)
+ #
+ # DB.from(:x).cross_join(gt)
+ # # SELECT * FROM "x" CROSS JOIN GRAPH_TABLE (...)
+ def graph_table(property_graph_name, initial_vertex_label, initial_vertex_opts=OPTS)
+ PropertyGraph::Table.create(property_graph_name, initial_vertex_label, initial_vertex_opts)
+ end
+
@@ -743,0 +1506,22 @@
+ # Array of symbols specifying property graphs in the current database.
+ # The dataset used is yielded to the block if one is provided,
+ # otherwise, an array of symbols of property graph names is returned.
+ # Supported on PostgreSQL 19+, will be an empty array on lower versions.
+ #
+ # Options:
+ # :qualify :: Return the property graph names as Sequel::SQL::QualifiedIdentifier
+ # instances, using the schema the property graph is located in as the qualifier.
+ # :schema :: The schema to search
+ # :server :: The server to use
+ def property_graphs(opts=OPTS, &block)
+ pg_class_relname('g', opts, &block)
+ end
+
+ # Rename a property graph.
+ #
+ # DB.rename_property_graph(:x, :y)
+ # # ALTER PROPERTY GRAPH x RENAME TO y
+ def rename_property_graph(old_name, new_name)
+ execute_ddl("ALTER PROPERTY GRAPH #{literal(old_name)} RENAME TO #{literal(new_name)}".freeze)
+ end
+
@@ -806,0 +1591,10 @@
+ # Change the schema for a property graph. Options:
+ # :if_exists :: Use the IF EXISTS clause to not raise an error if the
+ # property graph does not exist.
+ #
+ # DB.set_property_graph_schema(:x, :y)
+ # # ALTER PROPERTY GRAPH x SET SCHEMA y
+ def set_property_graph_schema(old_name, new_name, opts=OPTS)
+ execute_ddl("ALTER PROPERTY GRAPH#{" IF EXISTS" if opts[:if_exists]} #{literal(old_name)} SET SCHEMA #{literal(new_name)}".freeze)
+ end
+
@@ -1206,0 +2001,56 @@
+ # SQL statement for a single ALTER PROPERTY GRAPH operation.
+ def alter_property_graph_op_sql(name, op)
+ sql = String.new << "ALTER PROPERTY GRAPH " << quote_schema_table(name) << " "
+
+ case op_type = op[:op]
+ when :add_vertex_tables
+ sql << "ADD VERTEX TABLES (" <<
+ op[:tables].map do |vertex|
+ create_property_graph_table_sql(vertex) <<
+ create_property_graph_labels_sql(vertex.labels)
+ end.join(', ') << ")"
+ when :add_edge_tables
+ sql << "ADD EDGE TABLES (" <<
+ op[:tables].map do |edge|
+ create_property_graph_table_sql(edge) <<
+ " SOURCE " << create_property_graph_edge_side_sql(edge.source) <<
+ " DESTINATION " << create_property_graph_edge_side_sql(edge.destination) <<
+ create_property_graph_labels_sql(edge.labels)
+ end.join(', ') << ")"
+ when :drop_vertex_tables, :drop_edge_tables
+ sql << (op_type == :drop_vertex_tables ? "DROP VERTEX TABLES " : "DROP EDGE TABLES ") <<
+ literal(op[:aliases])
+ when :add_label
+ sql << alter_property_graph_element_table_sql(op)
+ op[:labels].each do |label_name, properties|
+ sql << " ADD LABEL " << quote_identifier(label_name) <<
+ create_property_graph_properties_clause_sql(properties)
+ end
+ when :drop_label
+ sql << alter_property_graph_element_table_sql(op) <<
+ " DROP LABEL " << quote_identifier(op[:label])
+ when :add_properties
+ sql << alter_property_graph_element_table_sql(op) <<
+ " ALTER LABEL " << quote_identifier(op[:label]) << " ADD PROPERTIES " <<
+ literal(op[:properties])
+ when :drop_properties
+ sql << alter_property_graph_element_table_sql(op) <<
+ " ALTER LABEL " << quote_identifier(op[:label]) << " DROP PROPERTIES " <<
+ literal(op[:properties])
+ else # when :set_owner
+ sql << "OWNER TO " << literal(op[:owner])
+ end
+
+ case op_type
+ when :drop_vertex_tables, :drop_edge_tables, :drop_label, :drop_properties
+ sql << " CASCADE" if op[:cascade]
+ end
+
+ sql
+ end
+
+ # SQL fragment for the ALTER PROPERTY GRAPH ALTER {VERTEX|EDGE} TABLE prefix
+ def alter_property_graph_element_table_sql(op)
+ "ALTER #{op[:kind] == :vertex ? 'VERTEX' : 'EDGE'} TABLE #{quote_identifier(op[:name])}"
+ end
+
@@ -1599,0 +2450,81 @@
+ # SQL statement for creating a property graph.
+ def create_property_graph_sql(name, data, opts=OPTS)
+ sql = String.new
+ sql << "CREATE "
+ sql << "TEMPORARY " if opts[:temp]
+ sql << "PROPERTY GRAPH "
+ sql << quote_schema_table(name)
+
+ unless data.vertices.empty?
+ sql << " VERTEX TABLES ("
+ sql << data.vertices.map do |vertex|
+ create_property_graph_table_sql(vertex) <<
+ create_property_graph_labels_sql(vertex.labels)
+ end.join(', ')
+ sql << ")"
+ end
+
+ unless data.edges.empty?
+ sql << " EDGE TABLES ("
+ sql << data.edges.map do |edge|
+ create_property_graph_table_sql(edge) <<
+ " SOURCE " << create_property_graph_edge_side_sql(edge.source) <<
+ " DESTINATION " << create_property_graph_edge_side_sql(edge.destination) <<
+ create_property_graph_labels_sql(edge.labels)
+ end.join(', ')
+ sql << ")"
+ end
+
+ sql
+ end
+
+ # SQL fragment for the SOURCE or DESTINATION clause of an edge in a property graph.
+ def create_property_graph_edge_side_sql(side)
+ sql = String.new
+ if side.key
+ sql << "KEY " << literal(side.key) << " REFERENCES "
+ end
+ sql << quote_identifier(side.name)
+ if side.references
+ sql << " " << literal(side.references)
+ end
+ sql
+ end
+
+ # SQL fragment for the table name and KEY clause used for vertices and edges in
+ # a property graph.
+ def create_property_graph_table_sql(element)
+ sql = String.new
+ sql << literal(element.name)
+
+ if key = element.key
+ sql << " KEY " << literal(key)
+ end
+
+ sql
+ end
+
+ # SQL fragment for the LABEL/PROPERTIES clauses used for vertices and
+ # edges in a property graph.
+ def create_property_graph_labels_sql(labels)
+ labels.map do |name, properties|
+ sql = String.new
+ sql << " LABEL " << quote_identifier(name) if name
+ sql << create_property_graph_properties_clause_sql(properties)
+ sql
+ end.join
+ end
+
+ # SQL fragment for the NO PROPERTIES, PROPERTIES ALL COLUMNS, or
+ # PROPERTIES (...) clause for a property graph element or label.
+ def create_property_graph_properties_clause_sql(properties)
+ case properties
+ when nil, :all
+ " PROPERTIES ALL COLUMNS"
+ when false, :none, [].freeze
+ " NO PROPERTIES"
+ else
+ " PROPERTIES #{literal(properties)}"
+ end
+ end
+
@@ -1712,0 +2644,5 @@
+ # SQL for dropping a property graph from the database.
+ def drop_property_graph_sql(name, opts=OPTS)
+ "DROP PROPERTY GRAPH#{' IF EXISTS' if opts[:if_exists]} #{literal(name)}#{' CASCADE' if opts[:cascade]}"
+ end
+
@@ -2135,0 +3072,20 @@
+ # Set FOR PORTION OF clause for UPDATE and DELETE statements.
+ # The first argument is the range or multirange column. If two arguments
+ # are provided, the second argument is an expression with the same
+ # database type as the first argument. If three arguments are provided,
+ # the second specifies the inclusive start of the portion to update and the third
+ # specifies the exclusive end of portion to update. When using the three argument
+ # form, nil can be provided as the second or third argument to have the start or
+ # end of the portion be unbounded. Supported on PostgreSQL 19+.
+ # Example:
+ #
+ # DB[:t].for_portion_of(:rc, Sequel.function(:int4range, 1, 2)).update(c: 3)
+ # # UPDATE t FOR PORTION OF rc (int4range(1, 2)) SET c = 3
+ #
+ # DB[:t].for_portion_of(:rc, 1, 2).update(c: 3)
+ # # UPDATE t FOR PORTION OF rc FROM 1 TO 2 SET c = 3
+ def for_portion_of(column, range, to=(arg_not_given=true))
+ range = [range, to].freeze unless arg_not_given
+ clone(:for_portion_of => [column, range].freeze)
+ end
+
@@ -2638 +3594,2 @@
- # Only include the primary table in the main delete clause
+ # Only include the primary table in the main delete clause.
+ # Support FOR PORTION OF.
@@ -2641 +3598 @@
- source_list_append(sql, @opts[:from][0..0])
+ table_for_portion_of_sql_append(sql)
@@ -2725,0 +3683,26 @@
+ # Add FOR PORTION OF SQL if the dataset uses it.
+ def table_for_portion_of_sql_append(sql)
+ fpo_column, fpo_range = @opts[:for_portion_of]
+ if fpo_column
+ table, aliaz = split_alias(@opts[:from].first)
+ source_list_append(sql, [table])
+ sql << ' FOR PORTION OF '
+ literal_append(sql, fpo_column)
+
+ if fpo_range.is_a?(Array)
+ fpo_start, fpo_end = fpo_range
+ sql << ' FROM '
+ literal_append(sql, fpo_start)
+ sql << ' TO '
+ literal_append(sql, fpo_end)
+ else
+ sql << ' ('
+ literal_append(sql, fpo_range)
+ sql << ')'
+ end
+ as_sql_append(sql, aliaz) if aliaz
+ else
+ source_list_append(sql, @opts[:from][0..0])
+ end
+ end
+
@@ -3015 +3998 @@
- # Only include the primary table in the main update clause
+ # Support FOR PORTION OF.
@@ -3018 +4001 @@
- source_list_append(sql, @opts[:from][0..0])
+ table_for_portion_of_sql_append(sql)
lib/sequel/adapters/shared/sqlite.rb
--- /tmp/d20260805-1596-i6n9tc/sequel-5.105.0/lib/sequel/adapters/shared/sqlite.rb 2026-08-05 02:33:52.470710648 +0000
+++ /tmp/d20260805-1596-i6n9tc/sequel-5.107.0/lib/sequel/adapters/shared/sqlite.rb 2026-08-05 02:33:52.518711308 +0000
@@ -680,3 +680,3 @@
- # Return an array of strings specifying a query explanation for a SELECT of the
- # current dataset. Currently, the options are ignored, but it accepts options
- # to be compatible with other adapters.
+ # Return a string specifying a query explanation for a SELECT of the
+ # current dataset. Options:
+ # :query_plan :: Use EXPLAIN QUERY PLAN instead of EXPLAIN if true.
@@ -687 +687,2 @@
- ds = db.send(:metadata_dataset).clone(:sql=>"EXPLAIN #{select_sql}".freeze)
+ keyword = (opts && opts[:query_plan]) ? "EXPLAIN QUERY PLAN" : "EXPLAIN"
+ ds = db.send(:metadata_dataset).clone(:sql=>"#{keyword} #{select_sql}".freeze)
lib/sequel/database/schema_generator.rb
--- /tmp/d20260805-1596-i6n9tc/sequel-5.105.0/lib/sequel/database/schema_generator.rb 2026-08-05 02:33:52.475710717 +0000
+++ /tmp/d20260805-1596-i6n9tc/sequel-5.107.0/lib/sequel/database/schema_generator.rb 2026-08-05 02:33:52.523711377 +0000
@@ -20 +20,3 @@
- break loc unless loc.path == __FILE__
+ # Skip core library methods implemented in Ruby
+ path = loc.path
+ break loc unless path == __FILE__ || (path && path.start_with?('<internal:'))
lib/sequel/dataset/sql.rb
--- /tmp/d20260805-1596-i6n9tc/sequel-5.105.0/lib/sequel/dataset/sql.rb 2026-08-05 02:33:52.479710772 +0000
+++ /tmp/d20260805-1596-i6n9tc/sequel-5.107.0/lib/sequel/dataset/sql.rb 2026-08-05 02:33:52.526711418 +0000
@@ -564,0 +565 @@
+ sql << ' IGNORE NULLS' if window.opts[:ignore_nulls]
lib/sequel/sql.rb
--- /tmp/d20260805-1596-i6n9tc/sequel-5.105.0/lib/sequel/sql.rb 2026-08-05 02:33:52.509711184 +0000
+++ /tmp/d20260805-1596-i6n9tc/sequel-5.107.0/lib/sequel/sql.rb 2026-08-05 02:33:52.568711995 +0000
@@ -1325,0 +1326 @@
+ ALL = Constant.new(:ALL)
@@ -1994,0 +1996,2 @@
+ # :ignore_nulls :: Can be set to :ignore for IGNORE NULLS (supported on PostgreSQL 19+ for
+ # a subset of default window functions)
lib/sequel/version.rb
--- /tmp/d20260805-1596-i6n9tc/sequel-5.105.0/lib/sequel/version.rb 2026-08-05 02:33:52.509711184 +0000
+++ /tmp/d20260805-1596-i6n9tc/sequel-5.107.0/lib/sequel/version.rb 2026-08-05 02:33:52.568711995 +0000
@@ -9 +9 @@
- MINOR = 105
+ MINOR = 107 |
Contributor
gem compare --diff sequel 5.105.0 5.107.0Compared versions: ["5.105.0", "5.107.0"]
DIFFERENT files:
5.105.0->5.107.0:
* Changed:
lib/sequel/adapters/shared/postgres.rb
--- /tmp/d20260805-1761-iz3inw/sequel-5.105.0/lib/sequel/adapters/shared/postgres.rb 2026-08-05 02:33:59.605791099 +0000
+++ /tmp/d20260805-1761-iz3inw/sequel-5.107.0/lib/sequel/adapters/shared/postgres.rb 2026-08-05 02:33:59.663790870 +0000
@@ -258,0 +259,558 @@
+ module PropertyGraph
+ # Base class for all Generator DSL classes. This uses a design where
+ # The DSL class is only used for the evaluation of the block, and new
+ # returns a frozen struct.
+ class Generator
+ # Instead of returning the Generator instance, return a frozen struct
+ # with data from the generator. This prevents accidentally calling the
+ # generator methods, and makes it possible for the generator class and
+ # result class to use the same method name in two different ways, with
+ # the generator setting data and the frozen struct method returning it.
+ # The frozen struct classes use the constant Data under each generator
+ # subclass.
+ def self.new(*args, &block)
+ super(*args, &block).data
+ end
+
+ # Base class for Vertex and Edge.
+ class Element < self
+ Data = Struct.new(:name, :key, :labels)
+
+ # +name+ specifies the name of the vertex or edge. It can be an
+ # SQL::AliasedExpression to use an alias. Options:
+ # :properties :: Specifies fixed properties for the vertex or edge.
+ # If this is given, you cannot use the label method
+ # inside the block.
+ def initialize(name, opts=OPTS, &block)
+ @name = name
+ @labels = []
+ if opts.key?(:properties)
+ @labels << [nil, opts[:properties]].freeze
+ @labels.freeze
+ end
+ instance_exec(&block) if block
+ @labels.freeze
+ freeze
+ end
+
+ def data
+ Data.new(@name, @key, @labels).freeze
+ end
+
+ # Set the column(s) to use for the KEY clause, which are the columns
+ # that uniquely identify rows in the table:
+ #
+ # key(:id)
+ # # KEY (id)
+ #
+ # key([:id1, :id2])
+ # # KEY (id1, id2)
+ def key(columns)
+ @key = Array(columns)
+ end
+
+ # Add a label and properties for the label for this vertex/edge.
+ # A vertex or edge can have multiple labels with separate properties,
+ # if it wasn't created with fixed properties. The +name+ argument
+ # specifies the label name. The +properties+ argument specifies the
+ # properties:
+ # nil, :all :: PROPERTIES ALL COLUMNS
+ # false, :none, [] :: NO PROPERTIES
+ # Array :: Array of specific properties. Each element should be a Symbol,
+ # SQL::Identifier, or SQL::AliasedExpression.
+ #
+ # label(:label_name)
+ # # LABEL label_name PROPERTIES ALL COLUMNS
+ #
+ # label(:label_name, [])
+ # # LABEL label_name NO PROPERTIES
+ #
+ # label(:label_name, [:c, Sequel[:b].as(:d)], Sequel[:e])
+ # # LABEL label_name PROPERTIES (c, b AS d, e)
+ def label(name, properties=:all)
+ if @labels.frozen?
+ raise Error, "cannot specify label for property graph vertex or edge with fixed properties"
+ end
+ @labels << [name, properties].freeze
+ nil
+ end
+ end
+
+ # Vertex is used for the block passed to Create#vertex, used to configure
+ # vertices in the property graph. It doesn't have any additional behavior
+ # compared to the Element class, so this is an alias instead of a subclass.
+ Vertex = Element
+
+ # Target is used for the block passed to Edge#source and Edge#destination,
+ # used to configure the source and destination of property graph edges.
+ class Target < self
+ Data = Struct.new(:name, :key, :references)
+
+ # +name+ specifies the name of the source or destination.
+ def initialize(name, &block)
+ @name = name
+ @key = nil
+ @references = nil
+ instance_exec(&block) if block
+ freeze
+ end
+
+ def data
+ Data.new(@name, @key, @references).freeze
+ end
+
+ # Set the column(s) to use for the KEY clause, which are the columns
+ # in the edge table that reference columns in the source or destination.
+ # Should be combined with #references to specify the columns being
+ # referenced.
+ #
+ # key(:vertex_id)
+ # # KEY (vertex_id)
+ #
+ # key([:vertex_id1, :vertex_id2])
+ # # KEY (vertex_id1, vertex_id2)
+ def key(keys)
+ @key = Array(keys)
+ end
+
+ # Set the column(s) to use for the REFERENCES clause, which are the columns
+ # in the source or destination table that are referenced by the edge table.
+ # Should be combined with #key to specify the columns doing the referencing.
+ #
+ # references(:id)
+ # # REFERENCES (id)
+ #
+ # references([:id1, :id2])
+ # # REFERENCES (id1, id2)
+ def references(refs)
+ @references = Array(refs)
+ end
+ end
+
+ # Edge is used for block passed to Create#edge, used to configure edges
+ # in the property graph.
+ class Edge < Element
+ Data = Struct.new(:name, :key, :labels, :source, :destination)
+
+ # In addition to inherited behavior, raises an error if a block
+ # is not passed or source or destination is not called in the block.
+ def initialize(name, opts=OPTS, &block)
+ super
+
+ unless @source && @destination
+ raise Error, "source and/or destination not defined for property graph edge"
+ end
+ end
+
+ def data
+ Data.new(@name, @key, @labels, @source, @destination).freeze
+ end
+
+ # Specify the source for the edge, with block evaluted by Target.
+ def source(name, &block)
+ raise Error, "cannot specify multiple sources for a property graph edge" if @source
+ @source = Target.new(name, &block)
+ end
+
+ # Specify the destination for the edge, with block evaluted by Target.
+ def destination(name, &block)
+ raise Error, "cannot specify multiple destinations for a property graph edge" if @destination
+ @destination = Target.new(name, &block)
+ end
+ end
+
+ # Create is used to evaluate the block given to DatabaseMethods#create_property_graph,
+ # used to specify the vertices and edges in the property graph.
+ class Create < self
+ Data = Struct.new(:vertices, :edges)
+
+ def initialize(&block)
+ @vertices = []
+ @edges = []
+ instance_exec(&block)
+ @vertices.freeze
+ @edges.freeze
+ freeze
+ end
+
+ def data
+ Data.new(@vertices, @edges).freeze
+ end
+
+ # Adds a vertex to the property graph, with the block evaluted by Vertex.
+ def vertex(name, opts=OPTS, &block)
+ @vertices << Vertex.new(name, opts, &block)
+ end
+
+ # Adds an edge to the property graph, with the block evaluted by Edge.
+ def edge(name, opts=OPTS, &block)
+ @edges << Edge.new(name, opts, &block)
+ end
+ end
+
+ # AlterElement is used to evaluate the block passed to
+ # Alter#alter_vertex_table and Alter#alter_edge_table.
+ class AlterElement < self
+ # +kind+ is +:vertex+ or +:edge+. +name+ is the alias of the
+ # vertex or edge table to alter.
+ def initialize(kind, name, &block)
+ @kind = kind
+ @name = name
+ @labels = []
+ @operations = []
+ instance_exec(&block)
+
+ # All labels added via #add_label are combined into a single
+ # ADD LABEL operation, as PostgreSQL supports adding multiple
+ # labels in a single ALTER ... ADD LABEL statement.
+ unless @labels.empty?
+ @operations << {:op=>:add_label, :kind=>kind, :name=>name, :labels=>@labels.freeze}
+ end
+
+ @operations.each(&:freeze)
+ @operations.freeze
+ freeze
+ end
+
+ def data
+ @operations
+ end
+
+ # Add a label (and optional properties) to the vertex/edge table.
+ # Takes the same arguments as Element#label. Can be called multiple
+ # times to add multiple labels.
+ #
+ # add_label(:l)
+ # # ADD LABEL l PROPERTIES ALL COLUMNS
+ def add_label(name, properties=:all)
+ @labels << [name, properties].freeze
+ nil
+ end
+
+ # Remove a label from the vertex/edge table. Options:
+ # :cascade :: Use CASCADE to drop dependent objects.
+ #
+ # drop_label(:l)
+ # # DROP LABEL l
+ def drop_label(name, opts=OPTS)
+ @operations << {:op=>:drop_label, :kind=>@kind, :name=>@name, :label=>name, :cascade=>opts[:cascade]}
+ nil
+ end
+
+ # Add properties to an existing label on the vertex/edge table.
+ # +properties+ is an expression, or array of expressions, the same
+ # as the explicit array form of the +properties+ argument to
+ # Element#label.
+ #
+ # add_properties(:l, [:c1, Sequel[:c2].as(:c3)])
+ # # ALTER LABEL l ADD PROPERTIES (c1, c2 AS c3)
+ def add_properties(label, properties)
+ @operations << {:op=>:add_properties, :kind=>@kind, :name=>@name, :label=>label, :properties=>Array(properties)}
+ nil
+ end
+
+ # Remove properties from an existing label on the vertex/edge table.
+ # +properties+ is a column name, or array of column names. Options:
+ # :cascade :: Use CASCADE to drop dependent objects.
+ #
+ # drop_properties(:l, [:c1])
+ # # ALTER LABEL l DROP PROPERTIES (c1)
+ def drop_properties(label, properties, opts=OPTS)
+ @operations << {:op=>:drop_properties, :kind=>@kind, :name=>@name, :label=>label, :properties=>Array(properties), :cascade=>opts[:cascade]}
+ nil
+ end
+ end
+
+ # Alter is used to evaluate the block given to DatabaseMethods#alter_property_graph,
+ # used to specify changes to an existing property graph.
+ class Alter < self
+ def initialize(&block)
+ @operations = []
+ instance_exec(&block)
+
+ @operations.each do |op|
+ case op[:op]
+ when :add_vertex_tables, :add_edge_tables
+ op[:tables].freeze
+ end
+ op.freeze
+ end
+ @operations.freeze
+ freeze
+ end
+
+ def data
+ @operations
+ end
+
+ # Add a vertex to the property graph, with the block used to configure the
+ # vertex.
+ #
+ # alter_property_graph.add_vertex(:v)
+ # # ADD VERTEX TABLES (v)
+ def add_vertex(name, opts=OPTS, &block)
+ add_tables_operation(:add_vertex_tables) << Vertex.new(name, opts, &block)
+ nil
+ end
+
+ # Add an edge to the property graph, with the block used to configure the edge.
+ #
+ # alter_property_graph.add_edge(:e){source :v1; destination :v2}
+ # # ADD EDGE TABLES (e SOURCE v1 DESTINATION v2)
+ def add_edge(name, opts=OPTS, &block)
+ add_tables_operation(:add_edge_tables) << Edge.new(name, opts, &block)
+ nil
+ end
+
+ # Remove vertex tables (referenced by their aliases) from the
+ # property graph. +aliases+ can be a single alias or an array.
+ # Options:
+ # :cascade :: Use CASCADE instead of the default RESTRICT.
+ #
+ # alter_property_graph.drop_vertex_tables([:v1, :v2])
+ # # DROP VERTEX TABLES (v1, v2)
+ def drop_vertex_tables(aliases, opts=OPTS)
+ @operations << {:op=>:drop_vertex_tables, :aliases=>Array(aliases), :cascade=>opts[:cascade]}
+ nil
+ end
+
+ # Remove edge tables (referenced by their aliases) from the property
+ # graph. See #drop_vertex_tables.
+ #
+ # alter_property_graph.drop_edge_tables([:e1, :e2])
+ # # DROP EDGE TABLES (e1, e2)
+ def drop_edge_tables(aliases, opts=OPTS)
+ @operations << {:op=>:drop_edge_tables, :aliases=>Array(aliases), :cascade=>opts[:cascade]}
+ nil
+ end
+
+ # Modify an existing vertex table (referenced by its alias).
+ #
+ # alter_property_graph.alter_vertex_table(:v){add_label :l}
+ # # ALTER VERTEX TABLE v ADD LABEL l PROPERTIES ALL COLUMNS
+ def alter_vertex_table(name, &block)
+ @operations.concat(AlterElement.new(:vertex, name, &block))
+ nil
+ end
+
+ # Modify an existing edge table (referenced by its alias).
+ #
+ # alter_property_graph.alter_edge_table(:e, properties: :none){drop_label :l}
+ # # ALTER VERTEX TABLE e DROP LABEL l
+ def alter_edge_table(name, &block)
+ @operations.concat(AlterElement.new(:edge, name, &block))
+ nil
+ end
+
+ # Change the owner of the property graph. +new_owner+ is usually a
+ # Symbol or SQL::Identifier for the role name, but can be
+ # <tt>Sequel.lit('CURRENT_USER')</tt> or
+ # <tt>Sequel.lit('SESSION_USER')</tt>.
+ #
+ # alter_property_graph.owner_to(:new_owner)
+ # # OWNER TO new_owner
+ def set_owner(new_owner)
+ @operations << {:op=>:set_owner, :owner=>new_owner}
+ nil
+ end
+
+ private
+
+ # Internals of add_vertex and add_edge.
+ def add_tables_operation(op_name)
+ unless op = @operations.find{|o| o[:op] == op_name}
+ @operations << (op = {:op=>op_name, :tables=>[]})
+ end
+ op[:tables]
+ end
+ end
+ end
+
+ # Represents a GRAPH_TABLE expression, used to query a property graph
+ # via graph pattern matching. This is used in place of a table name
+ # expression or dataset in a SELECT query. These are created by calling
+ # #graph_table on the related Database object.
+ #
+ # Table uses a method chaining design, similar to Dataset, where methods
+ # return modified frozen copies of the object.
+ class Table
+ include SQL::AliasMethods
+
+ # Internal struct for a single element (vertex or edge) in the graph pattern:
+ # +type+ :: Either :vertex or :edge.
+ # +marker+ :: Connector string to use for the element (empty for initial vertex).
+ # +label+ :: Label restriction symbol or SQL::Identifier for the element, if any.
+ # Can be an array or set to match multiple labels.
+ # +var+ :: Graph pattern variable symbol for the element, if any.
+ # +where+ :: WHERE condition for the element, if any.
+ Element = Struct.new(:type, :marker, :label, :var, :where) do
+ # Method used to create elements, used instead of new
+ # to ensure that the returned elements are frozen.
+ def self.create(type, marker, label, opts)
+ case label
+ when Array, Set
+ label = label.dup.freeze unless label.frozen?
+ end
+
+ case where = opts[:where]
+ when Hash, Array
+ where = SQL::BooleanExpression.from_value_pairs(where)
+ end
+
+ new(type, marker, label, opts[:var], where).freeze
+ end
+
+ private_class_method :new
+ end
+ private_constant :Element
+
+ # The name of the property graph the table is querying.
+ attr_reader :name
+
+ # A frozen array of Element instances, representing the vertices and
+ # edges in the graph pattern.
+ attr_reader :elements
+
+ # A frozen array of the columns used in the COLUMNS clause (aliased
+ # as columns_used, as #columns is used to modify the columns).
+ attr_reader :columns
+ alias columns_used columns
+
+ # Create a new Table with the given +graph_name+, with +initial_vertex_label+
+ # and +initial_vertex_opts+ being used to create the initial vertex.
+ # See Table#link for which options are supported for the initial vertex.
+ def self.create(graph_name, initial_vertex_label, initial_vertex_opts)
+ vertex = Element.create(:vertex, "", initial_vertex_label, initial_vertex_opts)
+ new(graph_name, [vertex].freeze, [].freeze)
+ end
+
+ def initialize(name, elements, columns)
+ @name = name
+ @elements = elements
+ @columns = columns
+ freeze
+ end
+
+ # Return a modified copy with an element added using a bidirectional link
+ # (<tt>-</tt> in the graph pattern).
+ # +label+ specifies the label restriction for the element. This can be
+ # nil for no label restriction, or an array or set to restrict to the
+ # given labels.
+ #
+ # Options supported:
+ # +:var+ :: Specifies a graph pattern variable name for the element,
+ # usable in the WHERE or COLUMNS clauses.
+ # +:vertex+ :: Specifies that the element being linked to is a vertex.
+ # This allows for direct vertex<->vertex linking, instead of
+ # the default vertex<->edge<->vertex linking.
+ # +:where+ :: An expression to use for the WHERE clause for the element.
+ #
+ # DB.graph_table(:gn, :v).link(:e)
+ # # GRAPH_TABLE (gn MATCH (IS v)-[IS e])
+ def link(label, opts=OPTS)
+ append_element('-', label, opts)
+ end
+
+ # Similar to #link, but uses a directed link from the previous element
+ # to the new element (<tt>-></tt> in the graph pattern). Accepts same
+ # arguments and options as #link.
+ #
+ # DB.graph_table(:gn, :v).to(:e)
+ # # GRAPH_TABLE (gn MATCH (IS v)->[IS e])
+ def to(label, opts=OPTS)
+ append_element('->', label, opts)
+ end
+
+ # Similar to #link, but uses a directed link from the new element
+ # to the previous element (<tt><-</tt> in the graph pattern). Accepts
+ # same arguments and options as #link.
+ #
+ # DB.graph_table(:gn, :v).from(:e)
+ # # GRAPH_TABLE (gn MATCH (IS v)<-[IS e])
+ def from(label, opts=OPTS)
+ append_element('<-', label, opts)
+ end
+
+ # Return a modifies copy that uses the given columns. A graph table
+ # must have a least one column set before it is used in a query.
+ #
+ # DB.graph_table(:gn, :v).columns(:a, Sequel[:b].as(:c))
+ # # GRAPH_TABLE (gn MATCH (IS v) COLUMNS (a, b AS c))
+ def columns(*cols)
+ self.class.new(@name, @elements, cols.freeze)
+ end
+
+ # Return a modified copy that adds the given columns to the existing
+ # list of columns for the graph table.
+ def add_columns(*cols)
+ columns(*@columns, *cols)
+ end
+
+ # Append the SQL for the GRAPH_TABLE expression to the given SQL string.
+ # Requires graph table have at least one column set.
+ def sql_literal_append(ds, sql)
+ if @columns.empty?
+ raise Error, "cannot use graph_table in a query if it does not return any columns"
+ end
+ if @elements.last.type == :edge
+ raise Error, "cannot use graph_table in a query if the last element is an edge"
+ end
+
+ sql << "GRAPH_TABLE ("
+ ds.literal_append(sql, @name)
+ sql << " MATCH "
+
+ @elements.each do |element|
+ marker = element.marker
+ var = element.var
+ label = element.label
+ where = element.where
+ vertex = element.type == :vertex
+
+ sql << marker
+ sql << (vertex ? '(' : '[')
+
+ ds.literal_append(sql, var) if var
+ if label
+ sql << (var ? " IS " : "IS ")
+ if label.is_a?(Array)
+ label_sep = ""
+ label.each do |l|
+ sql << label_sep
+ label_sep = "|" if label_sep.empty?
+ ds.literal_append(sql, l)
+ end
+ else
+ ds.literal_append(sql, label)
+ end
+ end
+
+ if where
+ sql << ((var || label) ? " WHERE " : "WHERE ")
+ ds.literal_append(sql, where)
+ end
+
+ sql << (vertex ? ')' : ']')
+ end
+
+ sql << " COLUMNS "
+ ds.literal_append(sql, @columns)
+ sql << ")"
+ end
+
+ private
+
+ # Internals of #link, #to, and #from.
+ def append_element(marker, label, opts)
+ node_type = if opts[:vertex]
+ :vertex
+ else
+ @elements.last.type == :vertex ? :edge : :vertex
+ end
+
+ element = Element.create(node_type, marker, label, opts)
+ self.class.new(@name, (@elements.dup << element).freeze, @columns)
+ end
+ end
+ end
+
@@ -340,0 +899,58 @@
+ # Alter the property graph with the given +name+, supported on PostgreSQL 19+.
+ # The block uses a DSL, evaluated by PropertyGraph::Generator::Alter. Example:
+ #
+ # DB.alter_property_graph(:my_graph) do
+ # # PropertyGraph::Generator::Alter
+ # add_vertex :companies2
+ # # ALTER PROPERTY GRAPH "my_graph" ADD VERTEX TABLES ("companies2")
+ #
+ # add_edge :works_at2 do
+ # # PropertyGraph::Generator::Edge
+ # source :people
+ # destination :companies2
+ # end
+ # # ALTER PROPERTY GRAPH "my_graph" ADD EDGE TABLES
+ # # ("works_at2" SOURCE "people" DESTINATION "companies2")
+ #
+ # drop_vertex_tables [:p2], cascade: true
+ # # ALTER PROPERTY GRAPH "my_graph" DROP VERTEX TABLES ("p2") CASCADE
+ #
+ # drop_edge_tables :e2
+ # # ALTER PROPERTY GRAPH "my_graph" DROP EDGE TABLES ("e2")
+ #
+ # alter_vertex_table :companies do
+ # # PropertyGraph::Generator::AlterElement
+ # add_label :public_company, [:name, :symbol]
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER VERTEX TABLE "companies"
+ # # ADD LABEL "public_company" PROPERTIES ("name", "symbol")
+ #
+ # drop_label :private_company
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER VERTEX TABLE "companies"
+ # # DROP LABEL "private_company"
+ #
+ # add_properties :company, :revenue
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER VERTEX TABLE "companies"
+ # # ALTER LABEL "company" ADD PROPERTIES ("revenue")
+ #
+ # drop_properties :company, :internal_id, cascade: true
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER VERTEX TABLE "companies"
+ # # ALTER LABEL "company" DROP PROPERTIES ("internal_id") CASCADE
+ # end
+ #
+ # alter_edge_table :works_at do
+ # # PropertyGraph::Generator::AlterElement
+ # add_label :employment
+ # end
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER EDGE TABLE "works_at"
+ # # ADD LABEL "employment" PROPERTIES ALL COLUMNS
+ #
+ # owner_to :new_owner
+ # # ALTER PROPERTY GRAPH "my_graph" OWNER TO "new_owner"
+ # end
+ def alter_property_graph(name, &block)
+ PropertyGraph::Generator::Alter.new(&block).each do |op|
+ execute_ddl(alter_property_graph_op_sql(name, op).freeze)
+ end
+ nil
+ end
+
@@ -469,0 +1086,61 @@
+ # Create a property graph in the database, supported on PostgreSQL 19+.
+ #
+ # Arguments:
+ # name :: Name of the property graph
+ # opts :: options hash:
+ # :temp :: Create the property graph as a temporary property graph.
+ #
+ # The block uses a DSL, with classes under PropertyGraph::Generator:
+ #
+ # DB.create_property_graph(:my_graph) do
+ # # PropertyGraph::Generator::Create
+ # vertex :people
+ #
+ # vertex Sequel.as(:people, :p), properties: []
+ #
+ # vertex Sequel.as(:companies, :c) do
+ # # PropertyGraph::Generator::Vertex
+ # key :id
+ # label :company
+ # label :c, [:name, (Sequel[:revenue] / 1000).as(:revenue_thousands)]
+ # end
+ #
+ # edge :works_at do
+ # # PropertyGraph::Generator::Edge
+ # source :people
+ # destination :c
+ # end
+ #
+ # edge Sequel.as(:employment, :e) do
+ # source :people do
+ # # PropertyGraph::Generator::Target
+ # key :person_id
+ # references :id
+ # end
+ # destination :c do
+ # # PropertyGraph::Generator::Target
+ # key :company_id
+ # references :id
+ # end
+ # label :employment
+ # end
+ # end
+ # # CREATE PROPERTY GRAPH "my_graph"
+ # # VERTEX TABLES (
+ # # "people",
+ # # "people" AS "p" NO PROPERTIES,
+ # # "companies" AS "c" KEY ("id")
+ # # LABEL "company" PROPERTIES ALL COLUMNS
+ # # LABEL "c" PROPERTIES ("name", ("revenue" / 1000) AS "revenue_thousands"))
+ # # EDGE TABLES (
+ # # "works_at"
+ # # SOURCE "people"
+ # # DESTINATION "c",
+ # # "employment" AS "e"
+ # # SOURCE KEY ("person_id") REFERENCES "people" ("id")
+ # # DESTINATION KEY ("company_id") REFERENCES "c" ("id")
+ # # LABEL "employment" PROPERTIES ALL COLUMNS)
+ def create_property_graph(name, opts=OPTS, &block)
+ execute_ddl(create_property_graph_sql(name, PropertyGraph::Generator::Create.new(&block), opts))
+ end
+
@@ -566,0 +1244,9 @@
+ # Drops a property graph from the database. Arguments:
+ # name :: name of the property graph to drop
+ # opts :: options hash:
+ # :cascade :: Drop other objects depending on this property_graph.
+ # :if_exists :: Don't raise an error if the property graph doesn't exist.
+ def drop_property_graph(name, opts=OPTS)
+ self << drop_property_graph_sql(name, opts).freeze
+ end
+
@@ -653,0 +1340,76 @@
+ # Return a PropertyGraph::Table instance for a property graph search
+ # (a GRAPH_TABLE clause for a SELECT query). Supported on PostgreSQL 19+.
+ #
+ # Arguments:
+ # +property_graph_name+ :: The property graph to query
+ # +initial_vertex_label+ :: The label restriction for the initial vertex for the
+ # graph pattern (can be nil for no label, or an array
+ # or set for restricting to one of multiple labels).
+ # +initial_vertex_opts+ :: The options for the initial vertex, see
+ # PropertyGraph::Table#link for available options.
+ #
+ # The returned instance should be further modified by calling methods on it,
+ # using a similar approach to how datasets work, where the methods return a
+ # modified copy of the receiver. The available methods:
+ #
+ # link :: Add a bidirectional link to a new element (vertex or edge)
+ # to :: Add a directional link from the last element to the new element
+ # from :: Add a direciton link from the new element to last element
+ # columns :: Replace the columns the graph table returns
+ # add_columns :: Append to the columns the graph table returns.
+ #
+ # See PropertyGraph::Table for the details of these methods and the arguments
+ # and options they support. Note that for a graph table to be usable in a query,
+ # it must return at least one column, and the last element in the graph pattern
+ # must be a vertex.
+ #
+ # gt = DB.graph_table(:pgn, :iv)
+ # # Not yet usable, does not return any columns
+ #
+ # # Set columns for graph table
+ # gt = gt.columns(:c, Sequel[1].as(:d))
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv") COLUMNS ("c", 1 AS "d"))
+ #
+ # # Adds directional link to edge, since last (initial) element was a vertex
+ # gt = gt.link(:e1)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"] COLUMNS ("c", 1 AS "d"))
+ #
+ # # Adds directional link from edge to vertex, since last element was an edge
+ # gt = gt.to(:v2)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2") COLUMNS ("c", 1 AS "d"))
+ #
+ # # Adds bidirection link from vertex to vertex (overriding the default)
+ # gt = gt.link(:v3, vertex: true)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3") COLUMNS ("c", 1 AS "d"))
+ #
+ # # Adds directional link from new edge to last vertex, since last element was an vertex.
+ # # Sets graph pattern variable name and uses it in a WHERE clause for the added element.
+ # gt = gt.from(:e2, var: :a2, where: {Sequel[:a2][:c] => 1})
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3")
+ # # <-["a2" IS "e2" WHERE ("a2"."c" = 1)] COLUMNS ("c", 1 AS "d"))
+ #
+ # # Can use nil as a label for no label restriction, both with and without a variable name
+ # gt = gt.to(nil).to(nil, var: :a3)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3")
+ # # <-["a2" IS "e2" WHERE ("a2"."c" = 1)]->[]->("a3") COLUMNS ("c", 1 AS "d"))
+ #
+ # # Can restrict to a one of a set of labels
+ # gt = gt.from([:x, :y], var: :a6)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3")
+ # # <-["a2" IS "e2" WHERE ("a2"."c" = 1)]->[]->("a3")->["a6" IS "x"|"y"] COLUMNS ("c", 1 AS "d"))
+ #
+ # # Add column(s) to the graph table
+ # gt = gt.add_columns(:y)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3")
+ # # <-["a2" IS "e2" WHERE ("a2"."c" = 1)]->[]->("a3")->["a6" IS "x"|"y"]
+ # # COLUMNS ("c", 1 AS "d", "y"))
+ #
+ # DB.from(gt)
+ # # SELECT * FROM GRAPH_TABLE (...)
+ #
+ # DB.from(:x).cross_join(gt)
+ # # SELECT * FROM "x" CROSS JOIN GRAPH_TABLE (...)
+ def graph_table(property_graph_name, initial_vertex_label, initial_vertex_opts=OPTS)
+ PropertyGraph::Table.create(property_graph_name, initial_vertex_label, initial_vertex_opts)
+ end
+
@@ -743,0 +1506,22 @@
+ # Array of symbols specifying property graphs in the current database.
+ # The dataset used is yielded to the block if one is provided,
+ # otherwise, an array of symbols of property graph names is returned.
+ # Supported on PostgreSQL 19+, will be an empty array on lower versions.
+ #
+ # Options:
+ # :qualify :: Return the property graph names as Sequel::SQL::QualifiedIdentifier
+ # instances, using the schema the property graph is located in as the qualifier.
+ # :schema :: The schema to search
+ # :server :: The server to use
+ def property_graphs(opts=OPTS, &block)
+ pg_class_relname('g', opts, &block)
+ end
+
+ # Rename a property graph.
+ #
+ # DB.rename_property_graph(:x, :y)
+ # # ALTER PROPERTY GRAPH x RENAME TO y
+ def rename_property_graph(old_name, new_name)
+ execute_ddl("ALTER PROPERTY GRAPH #{literal(old_name)} RENAME TO #{literal(new_name)}".freeze)
+ end
+
@@ -806,0 +1591,10 @@
+ # Change the schema for a property graph. Options:
+ # :if_exists :: Use the IF EXISTS clause to not raise an error if the
+ # property graph does not exist.
+ #
+ # DB.set_property_graph_schema(:x, :y)
+ # # ALTER PROPERTY GRAPH x SET SCHEMA y
+ def set_property_graph_schema(old_name, new_name, opts=OPTS)
+ execute_ddl("ALTER PROPERTY GRAPH#{" IF EXISTS" if opts[:if_exists]} #{literal(old_name)} SET SCHEMA #{literal(new_name)}".freeze)
+ end
+
@@ -1206,0 +2001,56 @@
+ # SQL statement for a single ALTER PROPERTY GRAPH operation.
+ def alter_property_graph_op_sql(name, op)
+ sql = String.new << "ALTER PROPERTY GRAPH " << quote_schema_table(name) << " "
+
+ case op_type = op[:op]
+ when :add_vertex_tables
+ sql << "ADD VERTEX TABLES (" <<
+ op[:tables].map do |vertex|
+ create_property_graph_table_sql(vertex) <<
+ create_property_graph_labels_sql(vertex.labels)
+ end.join(', ') << ")"
+ when :add_edge_tables
+ sql << "ADD EDGE TABLES (" <<
+ op[:tables].map do |edge|
+ create_property_graph_table_sql(edge) <<
+ " SOURCE " << create_property_graph_edge_side_sql(edge.source) <<
+ " DESTINATION " << create_property_graph_edge_side_sql(edge.destination) <<
+ create_property_graph_labels_sql(edge.labels)
+ end.join(', ') << ")"
+ when :drop_vertex_tables, :drop_edge_tables
+ sql << (op_type == :drop_vertex_tables ? "DROP VERTEX TABLES " : "DROP EDGE TABLES ") <<
+ literal(op[:aliases])
+ when :add_label
+ sql << alter_property_graph_element_table_sql(op)
+ op[:labels].each do |label_name, properties|
+ sql << " ADD LABEL " << quote_identifier(label_name) <<
+ create_property_graph_properties_clause_sql(properties)
+ end
+ when :drop_label
+ sql << alter_property_graph_element_table_sql(op) <<
+ " DROP LABEL " << quote_identifier(op[:label])
+ when :add_properties
+ sql << alter_property_graph_element_table_sql(op) <<
+ " ALTER LABEL " << quote_identifier(op[:label]) << " ADD PROPERTIES " <<
+ literal(op[:properties])
+ when :drop_properties
+ sql << alter_property_graph_element_table_sql(op) <<
+ " ALTER LABEL " << quote_identifier(op[:label]) << " DROP PROPERTIES " <<
+ literal(op[:properties])
+ else # when :set_owner
+ sql << "OWNER TO " << literal(op[:owner])
+ end
+
+ case op_type
+ when :drop_vertex_tables, :drop_edge_tables, :drop_label, :drop_properties
+ sql << " CASCADE" if op[:cascade]
+ end
+
+ sql
+ end
+
+ # SQL fragment for the ALTER PROPERTY GRAPH ALTER {VERTEX|EDGE} TABLE prefix
+ def alter_property_graph_element_table_sql(op)
+ "ALTER #{op[:kind] == :vertex ? 'VERTEX' : 'EDGE'} TABLE #{quote_identifier(op[:name])}"
+ end
+
@@ -1599,0 +2450,81 @@
+ # SQL statement for creating a property graph.
+ def create_property_graph_sql(name, data, opts=OPTS)
+ sql = String.new
+ sql << "CREATE "
+ sql << "TEMPORARY " if opts[:temp]
+ sql << "PROPERTY GRAPH "
+ sql << quote_schema_table(name)
+
+ unless data.vertices.empty?
+ sql << " VERTEX TABLES ("
+ sql << data.vertices.map do |vertex|
+ create_property_graph_table_sql(vertex) <<
+ create_property_graph_labels_sql(vertex.labels)
+ end.join(', ')
+ sql << ")"
+ end
+
+ unless data.edges.empty?
+ sql << " EDGE TABLES ("
+ sql << data.edges.map do |edge|
+ create_property_graph_table_sql(edge) <<
+ " SOURCE " << create_property_graph_edge_side_sql(edge.source) <<
+ " DESTINATION " << create_property_graph_edge_side_sql(edge.destination) <<
+ create_property_graph_labels_sql(edge.labels)
+ end.join(', ')
+ sql << ")"
+ end
+
+ sql
+ end
+
+ # SQL fragment for the SOURCE or DESTINATION clause of an edge in a property graph.
+ def create_property_graph_edge_side_sql(side)
+ sql = String.new
+ if side.key
+ sql << "KEY " << literal(side.key) << " REFERENCES "
+ end
+ sql << quote_identifier(side.name)
+ if side.references
+ sql << " " << literal(side.references)
+ end
+ sql
+ end
+
+ # SQL fragment for the table name and KEY clause used for vertices and edges in
+ # a property graph.
+ def create_property_graph_table_sql(element)
+ sql = String.new
+ sql << literal(element.name)
+
+ if key = element.key
+ sql << " KEY " << literal(key)
+ end
+
+ sql
+ end
+
+ # SQL fragment for the LABEL/PROPERTIES clauses used for vertices and
+ # edges in a property graph.
+ def create_property_graph_labels_sql(labels)
+ labels.map do |name, properties|
+ sql = String.new
+ sql << " LABEL " << quote_identifier(name) if name
+ sql << create_property_graph_properties_clause_sql(properties)
+ sql
+ end.join
+ end
+
+ # SQL fragment for the NO PROPERTIES, PROPERTIES ALL COLUMNS, or
+ # PROPERTIES (...) clause for a property graph element or label.
+ def create_property_graph_properties_clause_sql(properties)
+ case properties
+ when nil, :all
+ " PROPERTIES ALL COLUMNS"
+ when false, :none, [].freeze
+ " NO PROPERTIES"
+ else
+ " PROPERTIES #{literal(properties)}"
+ end
+ end
+
@@ -1712,0 +2644,5 @@
+ # SQL for dropping a property graph from the database.
+ def drop_property_graph_sql(name, opts=OPTS)
+ "DROP PROPERTY GRAPH#{' IF EXISTS' if opts[:if_exists]} #{literal(name)}#{' CASCADE' if opts[:cascade]}"
+ end
+
@@ -2135,0 +3072,20 @@
+ # Set FOR PORTION OF clause for UPDATE and DELETE statements.
+ # The first argument is the range or multirange column. If two arguments
+ # are provided, the second argument is an expression with the same
+ # database type as the first argument. If three arguments are provided,
+ # the second specifies the inclusive start of the portion to update and the third
+ # specifies the exclusive end of portion to update. When using the three argument
+ # form, nil can be provided as the second or third argument to have the start or
+ # end of the portion be unbounded. Supported on PostgreSQL 19+.
+ # Example:
+ #
+ # DB[:t].for_portion_of(:rc, Sequel.function(:int4range, 1, 2)).update(c: 3)
+ # # UPDATE t FOR PORTION OF rc (int4range(1, 2)) SET c = 3
+ #
+ # DB[:t].for_portion_of(:rc, 1, 2).update(c: 3)
+ # # UPDATE t FOR PORTION OF rc FROM 1 TO 2 SET c = 3
+ def for_portion_of(column, range, to=(arg_not_given=true))
+ range = [range, to].freeze unless arg_not_given
+ clone(:for_portion_of => [column, range].freeze)
+ end
+
@@ -2638 +3594,2 @@
- # Only include the primary table in the main delete clause
+ # Only include the primary table in the main delete clause.
+ # Support FOR PORTION OF.
@@ -2641 +3598 @@
- source_list_append(sql, @opts[:from][0..0])
+ table_for_portion_of_sql_append(sql)
@@ -2725,0 +3683,26 @@
+ # Add FOR PORTION OF SQL if the dataset uses it.
+ def table_for_portion_of_sql_append(sql)
+ fpo_column, fpo_range = @opts[:for_portion_of]
+ if fpo_column
+ table, aliaz = split_alias(@opts[:from].first)
+ source_list_append(sql, [table])
+ sql << ' FOR PORTION OF '
+ literal_append(sql, fpo_column)
+
+ if fpo_range.is_a?(Array)
+ fpo_start, fpo_end = fpo_range
+ sql << ' FROM '
+ literal_append(sql, fpo_start)
+ sql << ' TO '
+ literal_append(sql, fpo_end)
+ else
+ sql << ' ('
+ literal_append(sql, fpo_range)
+ sql << ')'
+ end
+ as_sql_append(sql, aliaz) if aliaz
+ else
+ source_list_append(sql, @opts[:from][0..0])
+ end
+ end
+
@@ -3015 +3998 @@
- # Only include the primary table in the main update clause
+ # Support FOR PORTION OF.
@@ -3018 +4001 @@
- source_list_append(sql, @opts[:from][0..0])
+ table_for_portion_of_sql_append(sql)
lib/sequel/adapters/shared/sqlite.rb
--- /tmp/d20260805-1761-iz3inw/sequel-5.105.0/lib/sequel/adapters/shared/sqlite.rb 2026-08-05 02:33:59.606791095 +0000
+++ /tmp/d20260805-1761-iz3inw/sequel-5.107.0/lib/sequel/adapters/shared/sqlite.rb 2026-08-05 02:33:59.664790866 +0000
@@ -680,3 +680,3 @@
- # Return an array of strings specifying a query explanation for a SELECT of the
- # current dataset. Currently, the options are ignored, but it accepts options
- # to be compatible with other adapters.
+ # Return a string specifying a query explanation for a SELECT of the
+ # current dataset. Options:
+ # :query_plan :: Use EXPLAIN QUERY PLAN instead of EXPLAIN if true.
@@ -687 +687,2 @@
- ds = db.send(:metadata_dataset).clone(:sql=>"EXPLAIN #{select_sql}".freeze)
+ keyword = (opts && opts[:query_plan]) ? "EXPLAIN QUERY PLAN" : "EXPLAIN"
+ ds = db.send(:metadata_dataset).clone(:sql=>"#{keyword} #{select_sql}".freeze)
lib/sequel/database/schema_generator.rb
--- /tmp/d20260805-1761-iz3inw/sequel-5.105.0/lib/sequel/database/schema_generator.rb 2026-08-05 02:33:59.612791071 +0000
+++ /tmp/d20260805-1761-iz3inw/sequel-5.107.0/lib/sequel/database/schema_generator.rb 2026-08-05 02:33:59.671790839 +0000
@@ -20 +20,3 @@
- break loc unless loc.path == __FILE__
+ # Skip core library methods implemented in Ruby
+ path = loc.path
+ break loc unless path == __FILE__ || (path && path.start_with?('<internal:'))
lib/sequel/dataset/sql.rb
--- /tmp/d20260805-1761-iz3inw/sequel-5.105.0/lib/sequel/dataset/sql.rb 2026-08-05 02:33:59.615791060 +0000
+++ /tmp/d20260805-1761-iz3inw/sequel-5.107.0/lib/sequel/dataset/sql.rb 2026-08-05 02:33:59.677790815 +0000
@@ -564,0 +565 @@
+ sql << ' IGNORE NULLS' if window.opts[:ignore_nulls]
lib/sequel/sql.rb
--- /tmp/d20260805-1761-iz3inw/sequel-5.105.0/lib/sequel/sql.rb 2026-08-05 02:33:59.651790918 +0000
+++ /tmp/d20260805-1761-iz3inw/sequel-5.107.0/lib/sequel/sql.rb 2026-08-05 02:33:59.716790661 +0000
@@ -1325,0 +1326 @@
+ ALL = Constant.new(:ALL)
@@ -1994,0 +1996,2 @@
+ # :ignore_nulls :: Can be set to :ignore for IGNORE NULLS (supported on PostgreSQL 19+ for
+ # a subset of default window functions)
lib/sequel/version.rb
--- /tmp/d20260805-1761-iz3inw/sequel-5.105.0/lib/sequel/version.rb 2026-08-05 02:33:59.651790918 +0000
+++ /tmp/d20260805-1761-iz3inw/sequel-5.107.0/lib/sequel/version.rb 2026-08-05 02:33:59.717790658 +0000
@@ -9 +9 @@
- MINOR = 105
+ MINOR = 107 |
Contributor
gem compare sequel 5.105.0 5.107.0Compared versions: ["5.105.0", "5.107.0"]
DIFFERENT rubygems_version:
5.105.0: 4.0.10
5.107.0: 4.0.16
DIFFERENT version:
5.105.0: 5.105.0
5.107.0: 5.107.0
DIFFERENT files:
5.105.0->5.107.0:
* Changed:
lib/sequel/adapters/shared/postgres.rb +987/-4
lib/sequel/adapters/shared/sqlite.rb +5/-4
lib/sequel/database/schema_generator.rb +3/-1
lib/sequel/dataset/sql.rb +1/-0
lib/sequel/sql.rb +3/-0
lib/sequel/version.rb +1/-1 |
Contributor
gem compare --diff sequel 5.105.0 5.107.0Compared versions: ["5.105.0", "5.107.0"]
DIFFERENT files:
5.105.0->5.107.0:
* Changed:
lib/sequel/adapters/shared/postgres.rb
--- /tmp/d20260805-1603-5t7wq4/sequel-5.105.0/lib/sequel/adapters/shared/postgres.rb 2026-08-05 02:34:27.002586858 +0000
+++ /tmp/d20260805-1603-5t7wq4/sequel-5.107.0/lib/sequel/adapters/shared/postgres.rb 2026-08-05 02:34:27.054586808 +0000
@@ -258,0 +259,558 @@
+ module PropertyGraph
+ # Base class for all Generator DSL classes. This uses a design where
+ # The DSL class is only used for the evaluation of the block, and new
+ # returns a frozen struct.
+ class Generator
+ # Instead of returning the Generator instance, return a frozen struct
+ # with data from the generator. This prevents accidentally calling the
+ # generator methods, and makes it possible for the generator class and
+ # result class to use the same method name in two different ways, with
+ # the generator setting data and the frozen struct method returning it.
+ # The frozen struct classes use the constant Data under each generator
+ # subclass.
+ def self.new(*args, &block)
+ super(*args, &block).data
+ end
+
+ # Base class for Vertex and Edge.
+ class Element < self
+ Data = Struct.new(:name, :key, :labels)
+
+ # +name+ specifies the name of the vertex or edge. It can be an
+ # SQL::AliasedExpression to use an alias. Options:
+ # :properties :: Specifies fixed properties for the vertex or edge.
+ # If this is given, you cannot use the label method
+ # inside the block.
+ def initialize(name, opts=OPTS, &block)
+ @name = name
+ @labels = []
+ if opts.key?(:properties)
+ @labels << [nil, opts[:properties]].freeze
+ @labels.freeze
+ end
+ instance_exec(&block) if block
+ @labels.freeze
+ freeze
+ end
+
+ def data
+ Data.new(@name, @key, @labels).freeze
+ end
+
+ # Set the column(s) to use for the KEY clause, which are the columns
+ # that uniquely identify rows in the table:
+ #
+ # key(:id)
+ # # KEY (id)
+ #
+ # key([:id1, :id2])
+ # # KEY (id1, id2)
+ def key(columns)
+ @key = Array(columns)
+ end
+
+ # Add a label and properties for the label for this vertex/edge.
+ # A vertex or edge can have multiple labels with separate properties,
+ # if it wasn't created with fixed properties. The +name+ argument
+ # specifies the label name. The +properties+ argument specifies the
+ # properties:
+ # nil, :all :: PROPERTIES ALL COLUMNS
+ # false, :none, [] :: NO PROPERTIES
+ # Array :: Array of specific properties. Each element should be a Symbol,
+ # SQL::Identifier, or SQL::AliasedExpression.
+ #
+ # label(:label_name)
+ # # LABEL label_name PROPERTIES ALL COLUMNS
+ #
+ # label(:label_name, [])
+ # # LABEL label_name NO PROPERTIES
+ #
+ # label(:label_name, [:c, Sequel[:b].as(:d)], Sequel[:e])
+ # # LABEL label_name PROPERTIES (c, b AS d, e)
+ def label(name, properties=:all)
+ if @labels.frozen?
+ raise Error, "cannot specify label for property graph vertex or edge with fixed properties"
+ end
+ @labels << [name, properties].freeze
+ nil
+ end
+ end
+
+ # Vertex is used for the block passed to Create#vertex, used to configure
+ # vertices in the property graph. It doesn't have any additional behavior
+ # compared to the Element class, so this is an alias instead of a subclass.
+ Vertex = Element
+
+ # Target is used for the block passed to Edge#source and Edge#destination,
+ # used to configure the source and destination of property graph edges.
+ class Target < self
+ Data = Struct.new(:name, :key, :references)
+
+ # +name+ specifies the name of the source or destination.
+ def initialize(name, &block)
+ @name = name
+ @key = nil
+ @references = nil
+ instance_exec(&block) if block
+ freeze
+ end
+
+ def data
+ Data.new(@name, @key, @references).freeze
+ end
+
+ # Set the column(s) to use for the KEY clause, which are the columns
+ # in the edge table that reference columns in the source or destination.
+ # Should be combined with #references to specify the columns being
+ # referenced.
+ #
+ # key(:vertex_id)
+ # # KEY (vertex_id)
+ #
+ # key([:vertex_id1, :vertex_id2])
+ # # KEY (vertex_id1, vertex_id2)
+ def key(keys)
+ @key = Array(keys)
+ end
+
+ # Set the column(s) to use for the REFERENCES clause, which are the columns
+ # in the source or destination table that are referenced by the edge table.
+ # Should be combined with #key to specify the columns doing the referencing.
+ #
+ # references(:id)
+ # # REFERENCES (id)
+ #
+ # references([:id1, :id2])
+ # # REFERENCES (id1, id2)
+ def references(refs)
+ @references = Array(refs)
+ end
+ end
+
+ # Edge is used for block passed to Create#edge, used to configure edges
+ # in the property graph.
+ class Edge < Element
+ Data = Struct.new(:name, :key, :labels, :source, :destination)
+
+ # In addition to inherited behavior, raises an error if a block
+ # is not passed or source or destination is not called in the block.
+ def initialize(name, opts=OPTS, &block)
+ super
+
+ unless @source && @destination
+ raise Error, "source and/or destination not defined for property graph edge"
+ end
+ end
+
+ def data
+ Data.new(@name, @key, @labels, @source, @destination).freeze
+ end
+
+ # Specify the source for the edge, with block evaluted by Target.
+ def source(name, &block)
+ raise Error, "cannot specify multiple sources for a property graph edge" if @source
+ @source = Target.new(name, &block)
+ end
+
+ # Specify the destination for the edge, with block evaluted by Target.
+ def destination(name, &block)
+ raise Error, "cannot specify multiple destinations for a property graph edge" if @destination
+ @destination = Target.new(name, &block)
+ end
+ end
+
+ # Create is used to evaluate the block given to DatabaseMethods#create_property_graph,
+ # used to specify the vertices and edges in the property graph.
+ class Create < self
+ Data = Struct.new(:vertices, :edges)
+
+ def initialize(&block)
+ @vertices = []
+ @edges = []
+ instance_exec(&block)
+ @vertices.freeze
+ @edges.freeze
+ freeze
+ end
+
+ def data
+ Data.new(@vertices, @edges).freeze
+ end
+
+ # Adds a vertex to the property graph, with the block evaluted by Vertex.
+ def vertex(name, opts=OPTS, &block)
+ @vertices << Vertex.new(name, opts, &block)
+ end
+
+ # Adds an edge to the property graph, with the block evaluted by Edge.
+ def edge(name, opts=OPTS, &block)
+ @edges << Edge.new(name, opts, &block)
+ end
+ end
+
+ # AlterElement is used to evaluate the block passed to
+ # Alter#alter_vertex_table and Alter#alter_edge_table.
+ class AlterElement < self
+ # +kind+ is +:vertex+ or +:edge+. +name+ is the alias of the
+ # vertex or edge table to alter.
+ def initialize(kind, name, &block)
+ @kind = kind
+ @name = name
+ @labels = []
+ @operations = []
+ instance_exec(&block)
+
+ # All labels added via #add_label are combined into a single
+ # ADD LABEL operation, as PostgreSQL supports adding multiple
+ # labels in a single ALTER ... ADD LABEL statement.
+ unless @labels.empty?
+ @operations << {:op=>:add_label, :kind=>kind, :name=>name, :labels=>@labels.freeze}
+ end
+
+ @operations.each(&:freeze)
+ @operations.freeze
+ freeze
+ end
+
+ def data
+ @operations
+ end
+
+ # Add a label (and optional properties) to the vertex/edge table.
+ # Takes the same arguments as Element#label. Can be called multiple
+ # times to add multiple labels.
+ #
+ # add_label(:l)
+ # # ADD LABEL l PROPERTIES ALL COLUMNS
+ def add_label(name, properties=:all)
+ @labels << [name, properties].freeze
+ nil
+ end
+
+ # Remove a label from the vertex/edge table. Options:
+ # :cascade :: Use CASCADE to drop dependent objects.
+ #
+ # drop_label(:l)
+ # # DROP LABEL l
+ def drop_label(name, opts=OPTS)
+ @operations << {:op=>:drop_label, :kind=>@kind, :name=>@name, :label=>name, :cascade=>opts[:cascade]}
+ nil
+ end
+
+ # Add properties to an existing label on the vertex/edge table.
+ # +properties+ is an expression, or array of expressions, the same
+ # as the explicit array form of the +properties+ argument to
+ # Element#label.
+ #
+ # add_properties(:l, [:c1, Sequel[:c2].as(:c3)])
+ # # ALTER LABEL l ADD PROPERTIES (c1, c2 AS c3)
+ def add_properties(label, properties)
+ @operations << {:op=>:add_properties, :kind=>@kind, :name=>@name, :label=>label, :properties=>Array(properties)}
+ nil
+ end
+
+ # Remove properties from an existing label on the vertex/edge table.
+ # +properties+ is a column name, or array of column names. Options:
+ # :cascade :: Use CASCADE to drop dependent objects.
+ #
+ # drop_properties(:l, [:c1])
+ # # ALTER LABEL l DROP PROPERTIES (c1)
+ def drop_properties(label, properties, opts=OPTS)
+ @operations << {:op=>:drop_properties, :kind=>@kind, :name=>@name, :label=>label, :properties=>Array(properties), :cascade=>opts[:cascade]}
+ nil
+ end
+ end
+
+ # Alter is used to evaluate the block given to DatabaseMethods#alter_property_graph,
+ # used to specify changes to an existing property graph.
+ class Alter < self
+ def initialize(&block)
+ @operations = []
+ instance_exec(&block)
+
+ @operations.each do |op|
+ case op[:op]
+ when :add_vertex_tables, :add_edge_tables
+ op[:tables].freeze
+ end
+ op.freeze
+ end
+ @operations.freeze
+ freeze
+ end
+
+ def data
+ @operations
+ end
+
+ # Add a vertex to the property graph, with the block used to configure the
+ # vertex.
+ #
+ # alter_property_graph.add_vertex(:v)
+ # # ADD VERTEX TABLES (v)
+ def add_vertex(name, opts=OPTS, &block)
+ add_tables_operation(:add_vertex_tables) << Vertex.new(name, opts, &block)
+ nil
+ end
+
+ # Add an edge to the property graph, with the block used to configure the edge.
+ #
+ # alter_property_graph.add_edge(:e){source :v1; destination :v2}
+ # # ADD EDGE TABLES (e SOURCE v1 DESTINATION v2)
+ def add_edge(name, opts=OPTS, &block)
+ add_tables_operation(:add_edge_tables) << Edge.new(name, opts, &block)
+ nil
+ end
+
+ # Remove vertex tables (referenced by their aliases) from the
+ # property graph. +aliases+ can be a single alias or an array.
+ # Options:
+ # :cascade :: Use CASCADE instead of the default RESTRICT.
+ #
+ # alter_property_graph.drop_vertex_tables([:v1, :v2])
+ # # DROP VERTEX TABLES (v1, v2)
+ def drop_vertex_tables(aliases, opts=OPTS)
+ @operations << {:op=>:drop_vertex_tables, :aliases=>Array(aliases), :cascade=>opts[:cascade]}
+ nil
+ end
+
+ # Remove edge tables (referenced by their aliases) from the property
+ # graph. See #drop_vertex_tables.
+ #
+ # alter_property_graph.drop_edge_tables([:e1, :e2])
+ # # DROP EDGE TABLES (e1, e2)
+ def drop_edge_tables(aliases, opts=OPTS)
+ @operations << {:op=>:drop_edge_tables, :aliases=>Array(aliases), :cascade=>opts[:cascade]}
+ nil
+ end
+
+ # Modify an existing vertex table (referenced by its alias).
+ #
+ # alter_property_graph.alter_vertex_table(:v){add_label :l}
+ # # ALTER VERTEX TABLE v ADD LABEL l PROPERTIES ALL COLUMNS
+ def alter_vertex_table(name, &block)
+ @operations.concat(AlterElement.new(:vertex, name, &block))
+ nil
+ end
+
+ # Modify an existing edge table (referenced by its alias).
+ #
+ # alter_property_graph.alter_edge_table(:e, properties: :none){drop_label :l}
+ # # ALTER VERTEX TABLE e DROP LABEL l
+ def alter_edge_table(name, &block)
+ @operations.concat(AlterElement.new(:edge, name, &block))
+ nil
+ end
+
+ # Change the owner of the property graph. +new_owner+ is usually a
+ # Symbol or SQL::Identifier for the role name, but can be
+ # <tt>Sequel.lit('CURRENT_USER')</tt> or
+ # <tt>Sequel.lit('SESSION_USER')</tt>.
+ #
+ # alter_property_graph.owner_to(:new_owner)
+ # # OWNER TO new_owner
+ def set_owner(new_owner)
+ @operations << {:op=>:set_owner, :owner=>new_owner}
+ nil
+ end
+
+ private
+
+ # Internals of add_vertex and add_edge.
+ def add_tables_operation(op_name)
+ unless op = @operations.find{|o| o[:op] == op_name}
+ @operations << (op = {:op=>op_name, :tables=>[]})
+ end
+ op[:tables]
+ end
+ end
+ end
+
+ # Represents a GRAPH_TABLE expression, used to query a property graph
+ # via graph pattern matching. This is used in place of a table name
+ # expression or dataset in a SELECT query. These are created by calling
+ # #graph_table on the related Database object.
+ #
+ # Table uses a method chaining design, similar to Dataset, where methods
+ # return modified frozen copies of the object.
+ class Table
+ include SQL::AliasMethods
+
+ # Internal struct for a single element (vertex or edge) in the graph pattern:
+ # +type+ :: Either :vertex or :edge.
+ # +marker+ :: Connector string to use for the element (empty for initial vertex).
+ # +label+ :: Label restriction symbol or SQL::Identifier for the element, if any.
+ # Can be an array or set to match multiple labels.
+ # +var+ :: Graph pattern variable symbol for the element, if any.
+ # +where+ :: WHERE condition for the element, if any.
+ Element = Struct.new(:type, :marker, :label, :var, :where) do
+ # Method used to create elements, used instead of new
+ # to ensure that the returned elements are frozen.
+ def self.create(type, marker, label, opts)
+ case label
+ when Array, Set
+ label = label.dup.freeze unless label.frozen?
+ end
+
+ case where = opts[:where]
+ when Hash, Array
+ where = SQL::BooleanExpression.from_value_pairs(where)
+ end
+
+ new(type, marker, label, opts[:var], where).freeze
+ end
+
+ private_class_method :new
+ end
+ private_constant :Element
+
+ # The name of the property graph the table is querying.
+ attr_reader :name
+
+ # A frozen array of Element instances, representing the vertices and
+ # edges in the graph pattern.
+ attr_reader :elements
+
+ # A frozen array of the columns used in the COLUMNS clause (aliased
+ # as columns_used, as #columns is used to modify the columns).
+ attr_reader :columns
+ alias columns_used columns
+
+ # Create a new Table with the given +graph_name+, with +initial_vertex_label+
+ # and +initial_vertex_opts+ being used to create the initial vertex.
+ # See Table#link for which options are supported for the initial vertex.
+ def self.create(graph_name, initial_vertex_label, initial_vertex_opts)
+ vertex = Element.create(:vertex, "", initial_vertex_label, initial_vertex_opts)
+ new(graph_name, [vertex].freeze, [].freeze)
+ end
+
+ def initialize(name, elements, columns)
+ @name = name
+ @elements = elements
+ @columns = columns
+ freeze
+ end
+
+ # Return a modified copy with an element added using a bidirectional link
+ # (<tt>-</tt> in the graph pattern).
+ # +label+ specifies the label restriction for the element. This can be
+ # nil for no label restriction, or an array or set to restrict to the
+ # given labels.
+ #
+ # Options supported:
+ # +:var+ :: Specifies a graph pattern variable name for the element,
+ # usable in the WHERE or COLUMNS clauses.
+ # +:vertex+ :: Specifies that the element being linked to is a vertex.
+ # This allows for direct vertex<->vertex linking, instead of
+ # the default vertex<->edge<->vertex linking.
+ # +:where+ :: An expression to use for the WHERE clause for the element.
+ #
+ # DB.graph_table(:gn, :v).link(:e)
+ # # GRAPH_TABLE (gn MATCH (IS v)-[IS e])
+ def link(label, opts=OPTS)
+ append_element('-', label, opts)
+ end
+
+ # Similar to #link, but uses a directed link from the previous element
+ # to the new element (<tt>-></tt> in the graph pattern). Accepts same
+ # arguments and options as #link.
+ #
+ # DB.graph_table(:gn, :v).to(:e)
+ # # GRAPH_TABLE (gn MATCH (IS v)->[IS e])
+ def to(label, opts=OPTS)
+ append_element('->', label, opts)
+ end
+
+ # Similar to #link, but uses a directed link from the new element
+ # to the previous element (<tt><-</tt> in the graph pattern). Accepts
+ # same arguments and options as #link.
+ #
+ # DB.graph_table(:gn, :v).from(:e)
+ # # GRAPH_TABLE (gn MATCH (IS v)<-[IS e])
+ def from(label, opts=OPTS)
+ append_element('<-', label, opts)
+ end
+
+ # Return a modifies copy that uses the given columns. A graph table
+ # must have a least one column set before it is used in a query.
+ #
+ # DB.graph_table(:gn, :v).columns(:a, Sequel[:b].as(:c))
+ # # GRAPH_TABLE (gn MATCH (IS v) COLUMNS (a, b AS c))
+ def columns(*cols)
+ self.class.new(@name, @elements, cols.freeze)
+ end
+
+ # Return a modified copy that adds the given columns to the existing
+ # list of columns for the graph table.
+ def add_columns(*cols)
+ columns(*@columns, *cols)
+ end
+
+ # Append the SQL for the GRAPH_TABLE expression to the given SQL string.
+ # Requires graph table have at least one column set.
+ def sql_literal_append(ds, sql)
+ if @columns.empty?
+ raise Error, "cannot use graph_table in a query if it does not return any columns"
+ end
+ if @elements.last.type == :edge
+ raise Error, "cannot use graph_table in a query if the last element is an edge"
+ end
+
+ sql << "GRAPH_TABLE ("
+ ds.literal_append(sql, @name)
+ sql << " MATCH "
+
+ @elements.each do |element|
+ marker = element.marker
+ var = element.var
+ label = element.label
+ where = element.where
+ vertex = element.type == :vertex
+
+ sql << marker
+ sql << (vertex ? '(' : '[')
+
+ ds.literal_append(sql, var) if var
+ if label
+ sql << (var ? " IS " : "IS ")
+ if label.is_a?(Array)
+ label_sep = ""
+ label.each do |l|
+ sql << label_sep
+ label_sep = "|" if label_sep.empty?
+ ds.literal_append(sql, l)
+ end
+ else
+ ds.literal_append(sql, label)
+ end
+ end
+
+ if where
+ sql << ((var || label) ? " WHERE " : "WHERE ")
+ ds.literal_append(sql, where)
+ end
+
+ sql << (vertex ? ')' : ']')
+ end
+
+ sql << " COLUMNS "
+ ds.literal_append(sql, @columns)
+ sql << ")"
+ end
+
+ private
+
+ # Internals of #link, #to, and #from.
+ def append_element(marker, label, opts)
+ node_type = if opts[:vertex]
+ :vertex
+ else
+ @elements.last.type == :vertex ? :edge : :vertex
+ end
+
+ element = Element.create(node_type, marker, label, opts)
+ self.class.new(@name, (@elements.dup << element).freeze, @columns)
+ end
+ end
+ end
+
@@ -340,0 +899,58 @@
+ # Alter the property graph with the given +name+, supported on PostgreSQL 19+.
+ # The block uses a DSL, evaluated by PropertyGraph::Generator::Alter. Example:
+ #
+ # DB.alter_property_graph(:my_graph) do
+ # # PropertyGraph::Generator::Alter
+ # add_vertex :companies2
+ # # ALTER PROPERTY GRAPH "my_graph" ADD VERTEX TABLES ("companies2")
+ #
+ # add_edge :works_at2 do
+ # # PropertyGraph::Generator::Edge
+ # source :people
+ # destination :companies2
+ # end
+ # # ALTER PROPERTY GRAPH "my_graph" ADD EDGE TABLES
+ # # ("works_at2" SOURCE "people" DESTINATION "companies2")
+ #
+ # drop_vertex_tables [:p2], cascade: true
+ # # ALTER PROPERTY GRAPH "my_graph" DROP VERTEX TABLES ("p2") CASCADE
+ #
+ # drop_edge_tables :e2
+ # # ALTER PROPERTY GRAPH "my_graph" DROP EDGE TABLES ("e2")
+ #
+ # alter_vertex_table :companies do
+ # # PropertyGraph::Generator::AlterElement
+ # add_label :public_company, [:name, :symbol]
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER VERTEX TABLE "companies"
+ # # ADD LABEL "public_company" PROPERTIES ("name", "symbol")
+ #
+ # drop_label :private_company
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER VERTEX TABLE "companies"
+ # # DROP LABEL "private_company"
+ #
+ # add_properties :company, :revenue
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER VERTEX TABLE "companies"
+ # # ALTER LABEL "company" ADD PROPERTIES ("revenue")
+ #
+ # drop_properties :company, :internal_id, cascade: true
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER VERTEX TABLE "companies"
+ # # ALTER LABEL "company" DROP PROPERTIES ("internal_id") CASCADE
+ # end
+ #
+ # alter_edge_table :works_at do
+ # # PropertyGraph::Generator::AlterElement
+ # add_label :employment
+ # end
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER EDGE TABLE "works_at"
+ # # ADD LABEL "employment" PROPERTIES ALL COLUMNS
+ #
+ # owner_to :new_owner
+ # # ALTER PROPERTY GRAPH "my_graph" OWNER TO "new_owner"
+ # end
+ def alter_property_graph(name, &block)
+ PropertyGraph::Generator::Alter.new(&block).each do |op|
+ execute_ddl(alter_property_graph_op_sql(name, op).freeze)
+ end
+ nil
+ end
+
@@ -469,0 +1086,61 @@
+ # Create a property graph in the database, supported on PostgreSQL 19+.
+ #
+ # Arguments:
+ # name :: Name of the property graph
+ # opts :: options hash:
+ # :temp :: Create the property graph as a temporary property graph.
+ #
+ # The block uses a DSL, with classes under PropertyGraph::Generator:
+ #
+ # DB.create_property_graph(:my_graph) do
+ # # PropertyGraph::Generator::Create
+ # vertex :people
+ #
+ # vertex Sequel.as(:people, :p), properties: []
+ #
+ # vertex Sequel.as(:companies, :c) do
+ # # PropertyGraph::Generator::Vertex
+ # key :id
+ # label :company
+ # label :c, [:name, (Sequel[:revenue] / 1000).as(:revenue_thousands)]
+ # end
+ #
+ # edge :works_at do
+ # # PropertyGraph::Generator::Edge
+ # source :people
+ # destination :c
+ # end
+ #
+ # edge Sequel.as(:employment, :e) do
+ # source :people do
+ # # PropertyGraph::Generator::Target
+ # key :person_id
+ # references :id
+ # end
+ # destination :c do
+ # # PropertyGraph::Generator::Target
+ # key :company_id
+ # references :id
+ # end
+ # label :employment
+ # end
+ # end
+ # # CREATE PROPERTY GRAPH "my_graph"
+ # # VERTEX TABLES (
+ # # "people",
+ # # "people" AS "p" NO PROPERTIES,
+ # # "companies" AS "c" KEY ("id")
+ # # LABEL "company" PROPERTIES ALL COLUMNS
+ # # LABEL "c" PROPERTIES ("name", ("revenue" / 1000) AS "revenue_thousands"))
+ # # EDGE TABLES (
+ # # "works_at"
+ # # SOURCE "people"
+ # # DESTINATION "c",
+ # # "employment" AS "e"
+ # # SOURCE KEY ("person_id") REFERENCES "people" ("id")
+ # # DESTINATION KEY ("company_id") REFERENCES "c" ("id")
+ # # LABEL "employment" PROPERTIES ALL COLUMNS)
+ def create_property_graph(name, opts=OPTS, &block)
+ execute_ddl(create_property_graph_sql(name, PropertyGraph::Generator::Create.new(&block), opts))
+ end
+
@@ -566,0 +1244,9 @@
+ # Drops a property graph from the database. Arguments:
+ # name :: name of the property graph to drop
+ # opts :: options hash:
+ # :cascade :: Drop other objects depending on this property_graph.
+ # :if_exists :: Don't raise an error if the property graph doesn't exist.
+ def drop_property_graph(name, opts=OPTS)
+ self << drop_property_graph_sql(name, opts).freeze
+ end
+
@@ -653,0 +1340,76 @@
+ # Return a PropertyGraph::Table instance for a property graph search
+ # (a GRAPH_TABLE clause for a SELECT query). Supported on PostgreSQL 19+.
+ #
+ # Arguments:
+ # +property_graph_name+ :: The property graph to query
+ # +initial_vertex_label+ :: The label restriction for the initial vertex for the
+ # graph pattern (can be nil for no label, or an array
+ # or set for restricting to one of multiple labels).
+ # +initial_vertex_opts+ :: The options for the initial vertex, see
+ # PropertyGraph::Table#link for available options.
+ #
+ # The returned instance should be further modified by calling methods on it,
+ # using a similar approach to how datasets work, where the methods return a
+ # modified copy of the receiver. The available methods:
+ #
+ # link :: Add a bidirectional link to a new element (vertex or edge)
+ # to :: Add a directional link from the last element to the new element
+ # from :: Add a direciton link from the new element to last element
+ # columns :: Replace the columns the graph table returns
+ # add_columns :: Append to the columns the graph table returns.
+ #
+ # See PropertyGraph::Table for the details of these methods and the arguments
+ # and options they support. Note that for a graph table to be usable in a query,
+ # it must return at least one column, and the last element in the graph pattern
+ # must be a vertex.
+ #
+ # gt = DB.graph_table(:pgn, :iv)
+ # # Not yet usable, does not return any columns
+ #
+ # # Set columns for graph table
+ # gt = gt.columns(:c, Sequel[1].as(:d))
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv") COLUMNS ("c", 1 AS "d"))
+ #
+ # # Adds directional link to edge, since last (initial) element was a vertex
+ # gt = gt.link(:e1)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"] COLUMNS ("c", 1 AS "d"))
+ #
+ # # Adds directional link from edge to vertex, since last element was an edge
+ # gt = gt.to(:v2)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2") COLUMNS ("c", 1 AS "d"))
+ #
+ # # Adds bidirection link from vertex to vertex (overriding the default)
+ # gt = gt.link(:v3, vertex: true)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3") COLUMNS ("c", 1 AS "d"))
+ #
+ # # Adds directional link from new edge to last vertex, since last element was an vertex.
+ # # Sets graph pattern variable name and uses it in a WHERE clause for the added element.
+ # gt = gt.from(:e2, var: :a2, where: {Sequel[:a2][:c] => 1})
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3")
+ # # <-["a2" IS "e2" WHERE ("a2"."c" = 1)] COLUMNS ("c", 1 AS "d"))
+ #
+ # # Can use nil as a label for no label restriction, both with and without a variable name
+ # gt = gt.to(nil).to(nil, var: :a3)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3")
+ # # <-["a2" IS "e2" WHERE ("a2"."c" = 1)]->[]->("a3") COLUMNS ("c", 1 AS "d"))
+ #
+ # # Can restrict to a one of a set of labels
+ # gt = gt.from([:x, :y], var: :a6)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3")
+ # # <-["a2" IS "e2" WHERE ("a2"."c" = 1)]->[]->("a3")->["a6" IS "x"|"y"] COLUMNS ("c", 1 AS "d"))
+ #
+ # # Add column(s) to the graph table
+ # gt = gt.add_columns(:y)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3")
+ # # <-["a2" IS "e2" WHERE ("a2"."c" = 1)]->[]->("a3")->["a6" IS "x"|"y"]
+ # # COLUMNS ("c", 1 AS "d", "y"))
+ #
+ # DB.from(gt)
+ # # SELECT * FROM GRAPH_TABLE (...)
+ #
+ # DB.from(:x).cross_join(gt)
+ # # SELECT * FROM "x" CROSS JOIN GRAPH_TABLE (...)
+ def graph_table(property_graph_name, initial_vertex_label, initial_vertex_opts=OPTS)
+ PropertyGraph::Table.create(property_graph_name, initial_vertex_label, initial_vertex_opts)
+ end
+
@@ -743,0 +1506,22 @@
+ # Array of symbols specifying property graphs in the current database.
+ # The dataset used is yielded to the block if one is provided,
+ # otherwise, an array of symbols of property graph names is returned.
+ # Supported on PostgreSQL 19+, will be an empty array on lower versions.
+ #
+ # Options:
+ # :qualify :: Return the property graph names as Sequel::SQL::QualifiedIdentifier
+ # instances, using the schema the property graph is located in as the qualifier.
+ # :schema :: The schema to search
+ # :server :: The server to use
+ def property_graphs(opts=OPTS, &block)
+ pg_class_relname('g', opts, &block)
+ end
+
+ # Rename a property graph.
+ #
+ # DB.rename_property_graph(:x, :y)
+ # # ALTER PROPERTY GRAPH x RENAME TO y
+ def rename_property_graph(old_name, new_name)
+ execute_ddl("ALTER PROPERTY GRAPH #{literal(old_name)} RENAME TO #{literal(new_name)}".freeze)
+ end
+
@@ -806,0 +1591,10 @@
+ # Change the schema for a property graph. Options:
+ # :if_exists :: Use the IF EXISTS clause to not raise an error if the
+ # property graph does not exist.
+ #
+ # DB.set_property_graph_schema(:x, :y)
+ # # ALTER PROPERTY GRAPH x SET SCHEMA y
+ def set_property_graph_schema(old_name, new_name, opts=OPTS)
+ execute_ddl("ALTER PROPERTY GRAPH#{" IF EXISTS" if opts[:if_exists]} #{literal(old_name)} SET SCHEMA #{literal(new_name)}".freeze)
+ end
+
@@ -1206,0 +2001,56 @@
+ # SQL statement for a single ALTER PROPERTY GRAPH operation.
+ def alter_property_graph_op_sql(name, op)
+ sql = String.new << "ALTER PROPERTY GRAPH " << quote_schema_table(name) << " "
+
+ case op_type = op[:op]
+ when :add_vertex_tables
+ sql << "ADD VERTEX TABLES (" <<
+ op[:tables].map do |vertex|
+ create_property_graph_table_sql(vertex) <<
+ create_property_graph_labels_sql(vertex.labels)
+ end.join(', ') << ")"
+ when :add_edge_tables
+ sql << "ADD EDGE TABLES (" <<
+ op[:tables].map do |edge|
+ create_property_graph_table_sql(edge) <<
+ " SOURCE " << create_property_graph_edge_side_sql(edge.source) <<
+ " DESTINATION " << create_property_graph_edge_side_sql(edge.destination) <<
+ create_property_graph_labels_sql(edge.labels)
+ end.join(', ') << ")"
+ when :drop_vertex_tables, :drop_edge_tables
+ sql << (op_type == :drop_vertex_tables ? "DROP VERTEX TABLES " : "DROP EDGE TABLES ") <<
+ literal(op[:aliases])
+ when :add_label
+ sql << alter_property_graph_element_table_sql(op)
+ op[:labels].each do |label_name, properties|
+ sql << " ADD LABEL " << quote_identifier(label_name) <<
+ create_property_graph_properties_clause_sql(properties)
+ end
+ when :drop_label
+ sql << alter_property_graph_element_table_sql(op) <<
+ " DROP LABEL " << quote_identifier(op[:label])
+ when :add_properties
+ sql << alter_property_graph_element_table_sql(op) <<
+ " ALTER LABEL " << quote_identifier(op[:label]) << " ADD PROPERTIES " <<
+ literal(op[:properties])
+ when :drop_properties
+ sql << alter_property_graph_element_table_sql(op) <<
+ " ALTER LABEL " << quote_identifier(op[:label]) << " DROP PROPERTIES " <<
+ literal(op[:properties])
+ else # when :set_owner
+ sql << "OWNER TO " << literal(op[:owner])
+ end
+
+ case op_type
+ when :drop_vertex_tables, :drop_edge_tables, :drop_label, :drop_properties
+ sql << " CASCADE" if op[:cascade]
+ end
+
+ sql
+ end
+
+ # SQL fragment for the ALTER PROPERTY GRAPH ALTER {VERTEX|EDGE} TABLE prefix
+ def alter_property_graph_element_table_sql(op)
+ "ALTER #{op[:kind] == :vertex ? 'VERTEX' : 'EDGE'} TABLE #{quote_identifier(op[:name])}"
+ end
+
@@ -1599,0 +2450,81 @@
+ # SQL statement for creating a property graph.
+ def create_property_graph_sql(name, data, opts=OPTS)
+ sql = String.new
+ sql << "CREATE "
+ sql << "TEMPORARY " if opts[:temp]
+ sql << "PROPERTY GRAPH "
+ sql << quote_schema_table(name)
+
+ unless data.vertices.empty?
+ sql << " VERTEX TABLES ("
+ sql << data.vertices.map do |vertex|
+ create_property_graph_table_sql(vertex) <<
+ create_property_graph_labels_sql(vertex.labels)
+ end.join(', ')
+ sql << ")"
+ end
+
+ unless data.edges.empty?
+ sql << " EDGE TABLES ("
+ sql << data.edges.map do |edge|
+ create_property_graph_table_sql(edge) <<
+ " SOURCE " << create_property_graph_edge_side_sql(edge.source) <<
+ " DESTINATION " << create_property_graph_edge_side_sql(edge.destination) <<
+ create_property_graph_labels_sql(edge.labels)
+ end.join(', ')
+ sql << ")"
+ end
+
+ sql
+ end
+
+ # SQL fragment for the SOURCE or DESTINATION clause of an edge in a property graph.
+ def create_property_graph_edge_side_sql(side)
+ sql = String.new
+ if side.key
+ sql << "KEY " << literal(side.key) << " REFERENCES "
+ end
+ sql << quote_identifier(side.name)
+ if side.references
+ sql << " " << literal(side.references)
+ end
+ sql
+ end
+
+ # SQL fragment for the table name and KEY clause used for vertices and edges in
+ # a property graph.
+ def create_property_graph_table_sql(element)
+ sql = String.new
+ sql << literal(element.name)
+
+ if key = element.key
+ sql << " KEY " << literal(key)
+ end
+
+ sql
+ end
+
+ # SQL fragment for the LABEL/PROPERTIES clauses used for vertices and
+ # edges in a property graph.
+ def create_property_graph_labels_sql(labels)
+ labels.map do |name, properties|
+ sql = String.new
+ sql << " LABEL " << quote_identifier(name) if name
+ sql << create_property_graph_properties_clause_sql(properties)
+ sql
+ end.join
+ end
+
+ # SQL fragment for the NO PROPERTIES, PROPERTIES ALL COLUMNS, or
+ # PROPERTIES (...) clause for a property graph element or label.
+ def create_property_graph_properties_clause_sql(properties)
+ case properties
+ when nil, :all
+ " PROPERTIES ALL COLUMNS"
+ when false, :none, [].freeze
+ " NO PROPERTIES"
+ else
+ " PROPERTIES #{literal(properties)}"
+ end
+ end
+
@@ -1712,0 +2644,5 @@
+ # SQL for dropping a property graph from the database.
+ def drop_property_graph_sql(name, opts=OPTS)
+ "DROP PROPERTY GRAPH#{' IF EXISTS' if opts[:if_exists]} #{literal(name)}#{' CASCADE' if opts[:cascade]}"
+ end
+
@@ -2135,0 +3072,20 @@
+ # Set FOR PORTION OF clause for UPDATE and DELETE statements.
+ # The first argument is the range or multirange column. If two arguments
+ # are provided, the second argument is an expression with the same
+ # database type as the first argument. If three arguments are provided,
+ # the second specifies the inclusive start of the portion to update and the third
+ # specifies the exclusive end of portion to update. When using the three argument
+ # form, nil can be provided as the second or third argument to have the start or
+ # end of the portion be unbounded. Supported on PostgreSQL 19+.
+ # Example:
+ #
+ # DB[:t].for_portion_of(:rc, Sequel.function(:int4range, 1, 2)).update(c: 3)
+ # # UPDATE t FOR PORTION OF rc (int4range(1, 2)) SET c = 3
+ #
+ # DB[:t].for_portion_of(:rc, 1, 2).update(c: 3)
+ # # UPDATE t FOR PORTION OF rc FROM 1 TO 2 SET c = 3
+ def for_portion_of(column, range, to=(arg_not_given=true))
+ range = [range, to].freeze unless arg_not_given
+ clone(:for_portion_of => [column, range].freeze)
+ end
+
@@ -2638 +3594,2 @@
- # Only include the primary table in the main delete clause
+ # Only include the primary table in the main delete clause.
+ # Support FOR PORTION OF.
@@ -2641 +3598 @@
- source_list_append(sql, @opts[:from][0..0])
+ table_for_portion_of_sql_append(sql)
@@ -2725,0 +3683,26 @@
+ # Add FOR PORTION OF SQL if the dataset uses it.
+ def table_for_portion_of_sql_append(sql)
+ fpo_column, fpo_range = @opts[:for_portion_of]
+ if fpo_column
+ table, aliaz = split_alias(@opts[:from].first)
+ source_list_append(sql, [table])
+ sql << ' FOR PORTION OF '
+ literal_append(sql, fpo_column)
+
+ if fpo_range.is_a?(Array)
+ fpo_start, fpo_end = fpo_range
+ sql << ' FROM '
+ literal_append(sql, fpo_start)
+ sql << ' TO '
+ literal_append(sql, fpo_end)
+ else
+ sql << ' ('
+ literal_append(sql, fpo_range)
+ sql << ')'
+ end
+ as_sql_append(sql, aliaz) if aliaz
+ else
+ source_list_append(sql, @opts[:from][0..0])
+ end
+ end
+
@@ -3015 +3998 @@
- # Only include the primary table in the main update clause
+ # Support FOR PORTION OF.
@@ -3018 +4001 @@
- source_list_append(sql, @opts[:from][0..0])
+ table_for_portion_of_sql_append(sql)
lib/sequel/adapters/shared/sqlite.rb
--- /tmp/d20260805-1603-5t7wq4/sequel-5.105.0/lib/sequel/adapters/shared/sqlite.rb 2026-08-05 02:34:27.003586857 +0000
+++ /tmp/d20260805-1603-5t7wq4/sequel-5.107.0/lib/sequel/adapters/shared/sqlite.rb 2026-08-05 02:34:27.055586807 +0000
@@ -680,3 +680,3 @@
- # Return an array of strings specifying a query explanation for a SELECT of the
- # current dataset. Currently, the options are ignored, but it accepts options
- # to be compatible with other adapters.
+ # Return a string specifying a query explanation for a SELECT of the
+ # current dataset. Options:
+ # :query_plan :: Use EXPLAIN QUERY PLAN instead of EXPLAIN if true.
@@ -687 +687,2 @@
- ds = db.send(:metadata_dataset).clone(:sql=>"EXPLAIN #{select_sql}".freeze)
+ keyword = (opts && opts[:query_plan]) ? "EXPLAIN QUERY PLAN" : "EXPLAIN"
+ ds = db.send(:metadata_dataset).clone(:sql=>"#{keyword} #{select_sql}".freeze)
lib/sequel/database/schema_generator.rb
--- /tmp/d20260805-1603-5t7wq4/sequel-5.105.0/lib/sequel/database/schema_generator.rb 2026-08-05 02:34:27.007586853 +0000
+++ /tmp/d20260805-1603-5t7wq4/sequel-5.107.0/lib/sequel/database/schema_generator.rb 2026-08-05 02:34:27.067586796 +0000
@@ -20 +20,3 @@
- break loc unless loc.path == __FILE__
+ # Skip core library methods implemented in Ruby
+ path = loc.path
+ break loc unless path == __FILE__ || (path && path.start_with?('<internal:'))
lib/sequel/dataset/sql.rb
--- /tmp/d20260805-1603-5t7wq4/sequel-5.105.0/lib/sequel/dataset/sql.rb 2026-08-05 02:34:27.010586850 +0000
+++ /tmp/d20260805-1603-5t7wq4/sequel-5.107.0/lib/sequel/dataset/sql.rb 2026-08-05 02:34:27.070586793 +0000
@@ -564,0 +565 @@
+ sql << ' IGNORE NULLS' if window.opts[:ignore_nulls]
lib/sequel/sql.rb
--- /tmp/d20260805-1603-5t7wq4/sequel-5.105.0/lib/sequel/sql.rb 2026-08-05 02:34:27.046586816 +0000
+++ /tmp/d20260805-1603-5t7wq4/sequel-5.107.0/lib/sequel/sql.rb 2026-08-05 02:34:27.115586750 +0000
@@ -1325,0 +1326 @@
+ ALL = Constant.new(:ALL)
@@ -1994,0 +1996,2 @@
+ # :ignore_nulls :: Can be set to :ignore for IGNORE NULLS (supported on PostgreSQL 19+ for
+ # a subset of default window functions)
lib/sequel/version.rb
--- /tmp/d20260805-1603-5t7wq4/sequel-5.105.0/lib/sequel/version.rb 2026-08-05 02:34:27.047586815 +0000
+++ /tmp/d20260805-1603-5t7wq4/sequel-5.107.0/lib/sequel/version.rb 2026-08-05 02:34:27.116586749 +0000
@@ -9 +9 @@
- MINOR = 105
+ MINOR = 107 |
Contributor
gem compare sequel 5.105.0 5.107.0Compared versions: ["5.105.0", "5.107.0"]
DIFFERENT rubygems_version:
5.105.0: 4.0.10
5.107.0: 4.0.16
DIFFERENT version:
5.105.0: 5.105.0
5.107.0: 5.107.0
DIFFERENT files:
5.105.0->5.107.0:
* Changed:
lib/sequel/adapters/shared/postgres.rb +987/-4
lib/sequel/adapters/shared/sqlite.rb +5/-4
lib/sequel/database/schema_generator.rb +3/-1
lib/sequel/dataset/sql.rb +1/-0
lib/sequel/sql.rb +3/-0
lib/sequel/version.rb +1/-1 |
Contributor
gem compare --diff sequel 5.105.0 5.107.0Compared versions: ["5.105.0", "5.107.0"]
DIFFERENT files:
5.105.0->5.107.0:
* Changed:
lib/sequel/adapters/shared/postgres.rb
--- /tmp/d20260805-1553-q70zrr/sequel-5.105.0/lib/sequel/adapters/shared/postgres.rb 2026-08-05 02:35:11.216093014 +0000
+++ /tmp/d20260805-1553-q70zrr/sequel-5.107.0/lib/sequel/adapters/shared/postgres.rb 2026-08-05 02:35:11.540109820 +0000
@@ -258,0 +259,558 @@
+ module PropertyGraph
+ # Base class for all Generator DSL classes. This uses a design where
+ # The DSL class is only used for the evaluation of the block, and new
+ # returns a frozen struct.
+ class Generator
+ # Instead of returning the Generator instance, return a frozen struct
+ # with data from the generator. This prevents accidentally calling the
+ # generator methods, and makes it possible for the generator class and
+ # result class to use the same method name in two different ways, with
+ # the generator setting data and the frozen struct method returning it.
+ # The frozen struct classes use the constant Data under each generator
+ # subclass.
+ def self.new(*args, &block)
+ super(*args, &block).data
+ end
+
+ # Base class for Vertex and Edge.
+ class Element < self
+ Data = Struct.new(:name, :key, :labels)
+
+ # +name+ specifies the name of the vertex or edge. It can be an
+ # SQL::AliasedExpression to use an alias. Options:
+ # :properties :: Specifies fixed properties for the vertex or edge.
+ # If this is given, you cannot use the label method
+ # inside the block.
+ def initialize(name, opts=OPTS, &block)
+ @name = name
+ @labels = []
+ if opts.key?(:properties)
+ @labels << [nil, opts[:properties]].freeze
+ @labels.freeze
+ end
+ instance_exec(&block) if block
+ @labels.freeze
+ freeze
+ end
+
+ def data
+ Data.new(@name, @key, @labels).freeze
+ end
+
+ # Set the column(s) to use for the KEY clause, which are the columns
+ # that uniquely identify rows in the table:
+ #
+ # key(:id)
+ # # KEY (id)
+ #
+ # key([:id1, :id2])
+ # # KEY (id1, id2)
+ def key(columns)
+ @key = Array(columns)
+ end
+
+ # Add a label and properties for the label for this vertex/edge.
+ # A vertex or edge can have multiple labels with separate properties,
+ # if it wasn't created with fixed properties. The +name+ argument
+ # specifies the label name. The +properties+ argument specifies the
+ # properties:
+ # nil, :all :: PROPERTIES ALL COLUMNS
+ # false, :none, [] :: NO PROPERTIES
+ # Array :: Array of specific properties. Each element should be a Symbol,
+ # SQL::Identifier, or SQL::AliasedExpression.
+ #
+ # label(:label_name)
+ # # LABEL label_name PROPERTIES ALL COLUMNS
+ #
+ # label(:label_name, [])
+ # # LABEL label_name NO PROPERTIES
+ #
+ # label(:label_name, [:c, Sequel[:b].as(:d)], Sequel[:e])
+ # # LABEL label_name PROPERTIES (c, b AS d, e)
+ def label(name, properties=:all)
+ if @labels.frozen?
+ raise Error, "cannot specify label for property graph vertex or edge with fixed properties"
+ end
+ @labels << [name, properties].freeze
+ nil
+ end
+ end
+
+ # Vertex is used for the block passed to Create#vertex, used to configure
+ # vertices in the property graph. It doesn't have any additional behavior
+ # compared to the Element class, so this is an alias instead of a subclass.
+ Vertex = Element
+
+ # Target is used for the block passed to Edge#source and Edge#destination,
+ # used to configure the source and destination of property graph edges.
+ class Target < self
+ Data = Struct.new(:name, :key, :references)
+
+ # +name+ specifies the name of the source or destination.
+ def initialize(name, &block)
+ @name = name
+ @key = nil
+ @references = nil
+ instance_exec(&block) if block
+ freeze
+ end
+
+ def data
+ Data.new(@name, @key, @references).freeze
+ end
+
+ # Set the column(s) to use for the KEY clause, which are the columns
+ # in the edge table that reference columns in the source or destination.
+ # Should be combined with #references to specify the columns being
+ # referenced.
+ #
+ # key(:vertex_id)
+ # # KEY (vertex_id)
+ #
+ # key([:vertex_id1, :vertex_id2])
+ # # KEY (vertex_id1, vertex_id2)
+ def key(keys)
+ @key = Array(keys)
+ end
+
+ # Set the column(s) to use for the REFERENCES clause, which are the columns
+ # in the source or destination table that are referenced by the edge table.
+ # Should be combined with #key to specify the columns doing the referencing.
+ #
+ # references(:id)
+ # # REFERENCES (id)
+ #
+ # references([:id1, :id2])
+ # # REFERENCES (id1, id2)
+ def references(refs)
+ @references = Array(refs)
+ end
+ end
+
+ # Edge is used for block passed to Create#edge, used to configure edges
+ # in the property graph.
+ class Edge < Element
+ Data = Struct.new(:name, :key, :labels, :source, :destination)
+
+ # In addition to inherited behavior, raises an error if a block
+ # is not passed or source or destination is not called in the block.
+ def initialize(name, opts=OPTS, &block)
+ super
+
+ unless @source && @destination
+ raise Error, "source and/or destination not defined for property graph edge"
+ end
+ end
+
+ def data
+ Data.new(@name, @key, @labels, @source, @destination).freeze
+ end
+
+ # Specify the source for the edge, with block evaluted by Target.
+ def source(name, &block)
+ raise Error, "cannot specify multiple sources for a property graph edge" if @source
+ @source = Target.new(name, &block)
+ end
+
+ # Specify the destination for the edge, with block evaluted by Target.
+ def destination(name, &block)
+ raise Error, "cannot specify multiple destinations for a property graph edge" if @destination
+ @destination = Target.new(name, &block)
+ end
+ end
+
+ # Create is used to evaluate the block given to DatabaseMethods#create_property_graph,
+ # used to specify the vertices and edges in the property graph.
+ class Create < self
+ Data = Struct.new(:vertices, :edges)
+
+ def initialize(&block)
+ @vertices = []
+ @edges = []
+ instance_exec(&block)
+ @vertices.freeze
+ @edges.freeze
+ freeze
+ end
+
+ def data
+ Data.new(@vertices, @edges).freeze
+ end
+
+ # Adds a vertex to the property graph, with the block evaluted by Vertex.
+ def vertex(name, opts=OPTS, &block)
+ @vertices << Vertex.new(name, opts, &block)
+ end
+
+ # Adds an edge to the property graph, with the block evaluted by Edge.
+ def edge(name, opts=OPTS, &block)
+ @edges << Edge.new(name, opts, &block)
+ end
+ end
+
+ # AlterElement is used to evaluate the block passed to
+ # Alter#alter_vertex_table and Alter#alter_edge_table.
+ class AlterElement < self
+ # +kind+ is +:vertex+ or +:edge+. +name+ is the alias of the
+ # vertex or edge table to alter.
+ def initialize(kind, name, &block)
+ @kind = kind
+ @name = name
+ @labels = []
+ @operations = []
+ instance_exec(&block)
+
+ # All labels added via #add_label are combined into a single
+ # ADD LABEL operation, as PostgreSQL supports adding multiple
+ # labels in a single ALTER ... ADD LABEL statement.
+ unless @labels.empty?
+ @operations << {:op=>:add_label, :kind=>kind, :name=>name, :labels=>@labels.freeze}
+ end
+
+ @operations.each(&:freeze)
+ @operations.freeze
+ freeze
+ end
+
+ def data
+ @operations
+ end
+
+ # Add a label (and optional properties) to the vertex/edge table.
+ # Takes the same arguments as Element#label. Can be called multiple
+ # times to add multiple labels.
+ #
+ # add_label(:l)
+ # # ADD LABEL l PROPERTIES ALL COLUMNS
+ def add_label(name, properties=:all)
+ @labels << [name, properties].freeze
+ nil
+ end
+
+ # Remove a label from the vertex/edge table. Options:
+ # :cascade :: Use CASCADE to drop dependent objects.
+ #
+ # drop_label(:l)
+ # # DROP LABEL l
+ def drop_label(name, opts=OPTS)
+ @operations << {:op=>:drop_label, :kind=>@kind, :name=>@name, :label=>name, :cascade=>opts[:cascade]}
+ nil
+ end
+
+ # Add properties to an existing label on the vertex/edge table.
+ # +properties+ is an expression, or array of expressions, the same
+ # as the explicit array form of the +properties+ argument to
+ # Element#label.
+ #
+ # add_properties(:l, [:c1, Sequel[:c2].as(:c3)])
+ # # ALTER LABEL l ADD PROPERTIES (c1, c2 AS c3)
+ def add_properties(label, properties)
+ @operations << {:op=>:add_properties, :kind=>@kind, :name=>@name, :label=>label, :properties=>Array(properties)}
+ nil
+ end
+
+ # Remove properties from an existing label on the vertex/edge table.
+ # +properties+ is a column name, or array of column names. Options:
+ # :cascade :: Use CASCADE to drop dependent objects.
+ #
+ # drop_properties(:l, [:c1])
+ # # ALTER LABEL l DROP PROPERTIES (c1)
+ def drop_properties(label, properties, opts=OPTS)
+ @operations << {:op=>:drop_properties, :kind=>@kind, :name=>@name, :label=>label, :properties=>Array(properties), :cascade=>opts[:cascade]}
+ nil
+ end
+ end
+
+ # Alter is used to evaluate the block given to DatabaseMethods#alter_property_graph,
+ # used to specify changes to an existing property graph.
+ class Alter < self
+ def initialize(&block)
+ @operations = []
+ instance_exec(&block)
+
+ @operations.each do |op|
+ case op[:op]
+ when :add_vertex_tables, :add_edge_tables
+ op[:tables].freeze
+ end
+ op.freeze
+ end
+ @operations.freeze
+ freeze
+ end
+
+ def data
+ @operations
+ end
+
+ # Add a vertex to the property graph, with the block used to configure the
+ # vertex.
+ #
+ # alter_property_graph.add_vertex(:v)
+ # # ADD VERTEX TABLES (v)
+ def add_vertex(name, opts=OPTS, &block)
+ add_tables_operation(:add_vertex_tables) << Vertex.new(name, opts, &block)
+ nil
+ end
+
+ # Add an edge to the property graph, with the block used to configure the edge.
+ #
+ # alter_property_graph.add_edge(:e){source :v1; destination :v2}
+ # # ADD EDGE TABLES (e SOURCE v1 DESTINATION v2)
+ def add_edge(name, opts=OPTS, &block)
+ add_tables_operation(:add_edge_tables) << Edge.new(name, opts, &block)
+ nil
+ end
+
+ # Remove vertex tables (referenced by their aliases) from the
+ # property graph. +aliases+ can be a single alias or an array.
+ # Options:
+ # :cascade :: Use CASCADE instead of the default RESTRICT.
+ #
+ # alter_property_graph.drop_vertex_tables([:v1, :v2])
+ # # DROP VERTEX TABLES (v1, v2)
+ def drop_vertex_tables(aliases, opts=OPTS)
+ @operations << {:op=>:drop_vertex_tables, :aliases=>Array(aliases), :cascade=>opts[:cascade]}
+ nil
+ end
+
+ # Remove edge tables (referenced by their aliases) from the property
+ # graph. See #drop_vertex_tables.
+ #
+ # alter_property_graph.drop_edge_tables([:e1, :e2])
+ # # DROP EDGE TABLES (e1, e2)
+ def drop_edge_tables(aliases, opts=OPTS)
+ @operations << {:op=>:drop_edge_tables, :aliases=>Array(aliases), :cascade=>opts[:cascade]}
+ nil
+ end
+
+ # Modify an existing vertex table (referenced by its alias).
+ #
+ # alter_property_graph.alter_vertex_table(:v){add_label :l}
+ # # ALTER VERTEX TABLE v ADD LABEL l PROPERTIES ALL COLUMNS
+ def alter_vertex_table(name, &block)
+ @operations.concat(AlterElement.new(:vertex, name, &block))
+ nil
+ end
+
+ # Modify an existing edge table (referenced by its alias).
+ #
+ # alter_property_graph.alter_edge_table(:e, properties: :none){drop_label :l}
+ # # ALTER VERTEX TABLE e DROP LABEL l
+ def alter_edge_table(name, &block)
+ @operations.concat(AlterElement.new(:edge, name, &block))
+ nil
+ end
+
+ # Change the owner of the property graph. +new_owner+ is usually a
+ # Symbol or SQL::Identifier for the role name, but can be
+ # <tt>Sequel.lit('CURRENT_USER')</tt> or
+ # <tt>Sequel.lit('SESSION_USER')</tt>.
+ #
+ # alter_property_graph.owner_to(:new_owner)
+ # # OWNER TO new_owner
+ def set_owner(new_owner)
+ @operations << {:op=>:set_owner, :owner=>new_owner}
+ nil
+ end
+
+ private
+
+ # Internals of add_vertex and add_edge.
+ def add_tables_operation(op_name)
+ unless op = @operations.find{|o| o[:op] == op_name}
+ @operations << (op = {:op=>op_name, :tables=>[]})
+ end
+ op[:tables]
+ end
+ end
+ end
+
+ # Represents a GRAPH_TABLE expression, used to query a property graph
+ # via graph pattern matching. This is used in place of a table name
+ # expression or dataset in a SELECT query. These are created by calling
+ # #graph_table on the related Database object.
+ #
+ # Table uses a method chaining design, similar to Dataset, where methods
+ # return modified frozen copies of the object.
+ class Table
+ include SQL::AliasMethods
+
+ # Internal struct for a single element (vertex or edge) in the graph pattern:
+ # +type+ :: Either :vertex or :edge.
+ # +marker+ :: Connector string to use for the element (empty for initial vertex).
+ # +label+ :: Label restriction symbol or SQL::Identifier for the element, if any.
+ # Can be an array or set to match multiple labels.
+ # +var+ :: Graph pattern variable symbol for the element, if any.
+ # +where+ :: WHERE condition for the element, if any.
+ Element = Struct.new(:type, :marker, :label, :var, :where) do
+ # Method used to create elements, used instead of new
+ # to ensure that the returned elements are frozen.
+ def self.create(type, marker, label, opts)
+ case label
+ when Array, Set
+ label = label.dup.freeze unless label.frozen?
+ end
+
+ case where = opts[:where]
+ when Hash, Array
+ where = SQL::BooleanExpression.from_value_pairs(where)
+ end
+
+ new(type, marker, label, opts[:var], where).freeze
+ end
+
+ private_class_method :new
+ end
+ private_constant :Element
+
+ # The name of the property graph the table is querying.
+ attr_reader :name
+
+ # A frozen array of Element instances, representing the vertices and
+ # edges in the graph pattern.
+ attr_reader :elements
+
+ # A frozen array of the columns used in the COLUMNS clause (aliased
+ # as columns_used, as #columns is used to modify the columns).
+ attr_reader :columns
+ alias columns_used columns
+
+ # Create a new Table with the given +graph_name+, with +initial_vertex_label+
+ # and +initial_vertex_opts+ being used to create the initial vertex.
+ # See Table#link for which options are supported for the initial vertex.
+ def self.create(graph_name, initial_vertex_label, initial_vertex_opts)
+ vertex = Element.create(:vertex, "", initial_vertex_label, initial_vertex_opts)
+ new(graph_name, [vertex].freeze, [].freeze)
+ end
+
+ def initialize(name, elements, columns)
+ @name = name
+ @elements = elements
+ @columns = columns
+ freeze
+ end
+
+ # Return a modified copy with an element added using a bidirectional link
+ # (<tt>-</tt> in the graph pattern).
+ # +label+ specifies the label restriction for the element. This can be
+ # nil for no label restriction, or an array or set to restrict to the
+ # given labels.
+ #
+ # Options supported:
+ # +:var+ :: Specifies a graph pattern variable name for the element,
+ # usable in the WHERE or COLUMNS clauses.
+ # +:vertex+ :: Specifies that the element being linked to is a vertex.
+ # This allows for direct vertex<->vertex linking, instead of
+ # the default vertex<->edge<->vertex linking.
+ # +:where+ :: An expression to use for the WHERE clause for the element.
+ #
+ # DB.graph_table(:gn, :v).link(:e)
+ # # GRAPH_TABLE (gn MATCH (IS v)-[IS e])
+ def link(label, opts=OPTS)
+ append_element('-', label, opts)
+ end
+
+ # Similar to #link, but uses a directed link from the previous element
+ # to the new element (<tt>-></tt> in the graph pattern). Accepts same
+ # arguments and options as #link.
+ #
+ # DB.graph_table(:gn, :v).to(:e)
+ # # GRAPH_TABLE (gn MATCH (IS v)->[IS e])
+ def to(label, opts=OPTS)
+ append_element('->', label, opts)
+ end
+
+ # Similar to #link, but uses a directed link from the new element
+ # to the previous element (<tt><-</tt> in the graph pattern). Accepts
+ # same arguments and options as #link.
+ #
+ # DB.graph_table(:gn, :v).from(:e)
+ # # GRAPH_TABLE (gn MATCH (IS v)<-[IS e])
+ def from(label, opts=OPTS)
+ append_element('<-', label, opts)
+ end
+
+ # Return a modifies copy that uses the given columns. A graph table
+ # must have a least one column set before it is used in a query.
+ #
+ # DB.graph_table(:gn, :v).columns(:a, Sequel[:b].as(:c))
+ # # GRAPH_TABLE (gn MATCH (IS v) COLUMNS (a, b AS c))
+ def columns(*cols)
+ self.class.new(@name, @elements, cols.freeze)
+ end
+
+ # Return a modified copy that adds the given columns to the existing
+ # list of columns for the graph table.
+ def add_columns(*cols)
+ columns(*@columns, *cols)
+ end
+
+ # Append the SQL for the GRAPH_TABLE expression to the given SQL string.
+ # Requires graph table have at least one column set.
+ def sql_literal_append(ds, sql)
+ if @columns.empty?
+ raise Error, "cannot use graph_table in a query if it does not return any columns"
+ end
+ if @elements.last.type == :edge
+ raise Error, "cannot use graph_table in a query if the last element is an edge"
+ end
+
+ sql << "GRAPH_TABLE ("
+ ds.literal_append(sql, @name)
+ sql << " MATCH "
+
+ @elements.each do |element|
+ marker = element.marker
+ var = element.var
+ label = element.label
+ where = element.where
+ vertex = element.type == :vertex
+
+ sql << marker
+ sql << (vertex ? '(' : '[')
+
+ ds.literal_append(sql, var) if var
+ if label
+ sql << (var ? " IS " : "IS ")
+ if label.is_a?(Array)
+ label_sep = ""
+ label.each do |l|
+ sql << label_sep
+ label_sep = "|" if label_sep.empty?
+ ds.literal_append(sql, l)
+ end
+ else
+ ds.literal_append(sql, label)
+ end
+ end
+
+ if where
+ sql << ((var || label) ? " WHERE " : "WHERE ")
+ ds.literal_append(sql, where)
+ end
+
+ sql << (vertex ? ')' : ']')
+ end
+
+ sql << " COLUMNS "
+ ds.literal_append(sql, @columns)
+ sql << ")"
+ end
+
+ private
+
+ # Internals of #link, #to, and #from.
+ def append_element(marker, label, opts)
+ node_type = if opts[:vertex]
+ :vertex
+ else
+ @elements.last.type == :vertex ? :edge : :vertex
+ end
+
+ element = Element.create(node_type, marker, label, opts)
+ self.class.new(@name, (@elements.dup << element).freeze, @columns)
+ end
+ end
+ end
+
@@ -340,0 +899,58 @@
+ # Alter the property graph with the given +name+, supported on PostgreSQL 19+.
+ # The block uses a DSL, evaluated by PropertyGraph::Generator::Alter. Example:
+ #
+ # DB.alter_property_graph(:my_graph) do
+ # # PropertyGraph::Generator::Alter
+ # add_vertex :companies2
+ # # ALTER PROPERTY GRAPH "my_graph" ADD VERTEX TABLES ("companies2")
+ #
+ # add_edge :works_at2 do
+ # # PropertyGraph::Generator::Edge
+ # source :people
+ # destination :companies2
+ # end
+ # # ALTER PROPERTY GRAPH "my_graph" ADD EDGE TABLES
+ # # ("works_at2" SOURCE "people" DESTINATION "companies2")
+ #
+ # drop_vertex_tables [:p2], cascade: true
+ # # ALTER PROPERTY GRAPH "my_graph" DROP VERTEX TABLES ("p2") CASCADE
+ #
+ # drop_edge_tables :e2
+ # # ALTER PROPERTY GRAPH "my_graph" DROP EDGE TABLES ("e2")
+ #
+ # alter_vertex_table :companies do
+ # # PropertyGraph::Generator::AlterElement
+ # add_label :public_company, [:name, :symbol]
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER VERTEX TABLE "companies"
+ # # ADD LABEL "public_company" PROPERTIES ("name", "symbol")
+ #
+ # drop_label :private_company
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER VERTEX TABLE "companies"
+ # # DROP LABEL "private_company"
+ #
+ # add_properties :company, :revenue
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER VERTEX TABLE "companies"
+ # # ALTER LABEL "company" ADD PROPERTIES ("revenue")
+ #
+ # drop_properties :company, :internal_id, cascade: true
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER VERTEX TABLE "companies"
+ # # ALTER LABEL "company" DROP PROPERTIES ("internal_id") CASCADE
+ # end
+ #
+ # alter_edge_table :works_at do
+ # # PropertyGraph::Generator::AlterElement
+ # add_label :employment
+ # end
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER EDGE TABLE "works_at"
+ # # ADD LABEL "employment" PROPERTIES ALL COLUMNS
+ #
+ # owner_to :new_owner
+ # # ALTER PROPERTY GRAPH "my_graph" OWNER TO "new_owner"
+ # end
+ def alter_property_graph(name, &block)
+ PropertyGraph::Generator::Alter.new(&block).each do |op|
+ execute_ddl(alter_property_graph_op_sql(name, op).freeze)
+ end
+ nil
+ end
+
@@ -469,0 +1086,61 @@
+ # Create a property graph in the database, supported on PostgreSQL 19+.
+ #
+ # Arguments:
+ # name :: Name of the property graph
+ # opts :: options hash:
+ # :temp :: Create the property graph as a temporary property graph.
+ #
+ # The block uses a DSL, with classes under PropertyGraph::Generator:
+ #
+ # DB.create_property_graph(:my_graph) do
+ # # PropertyGraph::Generator::Create
+ # vertex :people
+ #
+ # vertex Sequel.as(:people, :p), properties: []
+ #
+ # vertex Sequel.as(:companies, :c) do
+ # # PropertyGraph::Generator::Vertex
+ # key :id
+ # label :company
+ # label :c, [:name, (Sequel[:revenue] / 1000).as(:revenue_thousands)]
+ # end
+ #
+ # edge :works_at do
+ # # PropertyGraph::Generator::Edge
+ # source :people
+ # destination :c
+ # end
+ #
+ # edge Sequel.as(:employment, :e) do
+ # source :people do
+ # # PropertyGraph::Generator::Target
+ # key :person_id
+ # references :id
+ # end
+ # destination :c do
+ # # PropertyGraph::Generator::Target
+ # key :company_id
+ # references :id
+ # end
+ # label :employment
+ # end
+ # end
+ # # CREATE PROPERTY GRAPH "my_graph"
+ # # VERTEX TABLES (
+ # # "people",
+ # # "people" AS "p" NO PROPERTIES,
+ # # "companies" AS "c" KEY ("id")
+ # # LABEL "company" PROPERTIES ALL COLUMNS
+ # # LABEL "c" PROPERTIES ("name", ("revenue" / 1000) AS "revenue_thousands"))
+ # # EDGE TABLES (
+ # # "works_at"
+ # # SOURCE "people"
+ # # DESTINATION "c",
+ # # "employment" AS "e"
+ # # SOURCE KEY ("person_id") REFERENCES "people" ("id")
+ # # DESTINATION KEY ("company_id") REFERENCES "c" ("id")
+ # # LABEL "employment" PROPERTIES ALL COLUMNS)
+ def create_property_graph(name, opts=OPTS, &block)
+ execute_ddl(create_property_graph_sql(name, PropertyGraph::Generator::Create.new(&block), opts))
+ end
+
@@ -566,0 +1244,9 @@
+ # Drops a property graph from the database. Arguments:
+ # name :: name of the property graph to drop
+ # opts :: options hash:
+ # :cascade :: Drop other objects depending on this property_graph.
+ # :if_exists :: Don't raise an error if the property graph doesn't exist.
+ def drop_property_graph(name, opts=OPTS)
+ self << drop_property_graph_sql(name, opts).freeze
+ end
+
@@ -653,0 +1340,76 @@
+ # Return a PropertyGraph::Table instance for a property graph search
+ # (a GRAPH_TABLE clause for a SELECT query). Supported on PostgreSQL 19+.
+ #
+ # Arguments:
+ # +property_graph_name+ :: The property graph to query
+ # +initial_vertex_label+ :: The label restriction for the initial vertex for the
+ # graph pattern (can be nil for no label, or an array
+ # or set for restricting to one of multiple labels).
+ # +initial_vertex_opts+ :: The options for the initial vertex, see
+ # PropertyGraph::Table#link for available options.
+ #
+ # The returned instance should be further modified by calling methods on it,
+ # using a similar approach to how datasets work, where the methods return a
+ # modified copy of the receiver. The available methods:
+ #
+ # link :: Add a bidirectional link to a new element (vertex or edge)
+ # to :: Add a directional link from the last element to the new element
+ # from :: Add a direciton link from the new element to last element
+ # columns :: Replace the columns the graph table returns
+ # add_columns :: Append to the columns the graph table returns.
+ #
+ # See PropertyGraph::Table for the details of these methods and the arguments
+ # and options they support. Note that for a graph table to be usable in a query,
+ # it must return at least one column, and the last element in the graph pattern
+ # must be a vertex.
+ #
+ # gt = DB.graph_table(:pgn, :iv)
+ # # Not yet usable, does not return any columns
+ #
+ # # Set columns for graph table
+ # gt = gt.columns(:c, Sequel[1].as(:d))
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv") COLUMNS ("c", 1 AS "d"))
+ #
+ # # Adds directional link to edge, since last (initial) element was a vertex
+ # gt = gt.link(:e1)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"] COLUMNS ("c", 1 AS "d"))
+ #
+ # # Adds directional link from edge to vertex, since last element was an edge
+ # gt = gt.to(:v2)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2") COLUMNS ("c", 1 AS "d"))
+ #
+ # # Adds bidirection link from vertex to vertex (overriding the default)
+ # gt = gt.link(:v3, vertex: true)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3") COLUMNS ("c", 1 AS "d"))
+ #
+ # # Adds directional link from new edge to last vertex, since last element was an vertex.
+ # # Sets graph pattern variable name and uses it in a WHERE clause for the added element.
+ # gt = gt.from(:e2, var: :a2, where: {Sequel[:a2][:c] => 1})
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3")
+ # # <-["a2" IS "e2" WHERE ("a2"."c" = 1)] COLUMNS ("c", 1 AS "d"))
+ #
+ # # Can use nil as a label for no label restriction, both with and without a variable name
+ # gt = gt.to(nil).to(nil, var: :a3)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3")
+ # # <-["a2" IS "e2" WHERE ("a2"."c" = 1)]->[]->("a3") COLUMNS ("c", 1 AS "d"))
+ #
+ # # Can restrict to a one of a set of labels
+ # gt = gt.from([:x, :y], var: :a6)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3")
+ # # <-["a2" IS "e2" WHERE ("a2"."c" = 1)]->[]->("a3")->["a6" IS "x"|"y"] COLUMNS ("c", 1 AS "d"))
+ #
+ # # Add column(s) to the graph table
+ # gt = gt.add_columns(:y)
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3")
+ # # <-["a2" IS "e2" WHERE ("a2"."c" = 1)]->[]->("a3")->["a6" IS "x"|"y"]
+ # # COLUMNS ("c", 1 AS "d", "y"))
+ #
+ # DB.from(gt)
+ # # SELECT * FROM GRAPH_TABLE (...)
+ #
+ # DB.from(:x).cross_join(gt)
+ # # SELECT * FROM "x" CROSS JOIN GRAPH_TABLE (...)
+ def graph_table(property_graph_name, initial_vertex_label, initial_vertex_opts=OPTS)
+ PropertyGraph::Table.create(property_graph_name, initial_vertex_label, initial_vertex_opts)
+ end
+
@@ -743,0 +1506,22 @@
+ # Array of symbols specifying property graphs in the current database.
+ # The dataset used is yielded to the block if one is provided,
+ # otherwise, an array of symbols of property graph names is returned.
+ # Supported on PostgreSQL 19+, will be an empty array on lower versions.
+ #
+ # Options:
+ # :qualify :: Return the property graph names as Sequel::SQL::QualifiedIdentifier
+ # instances, using the schema the property graph is located in as the qualifier.
+ # :schema :: The schema to search
+ # :server :: The server to use
+ def property_graphs(opts=OPTS, &block)
+ pg_class_relname('g', opts, &block)
+ end
+
+ # Rename a property graph.
+ #
+ # DB.rename_property_graph(:x, :y)
+ # # ALTER PROPERTY GRAPH x RENAME TO y
+ def rename_property_graph(old_name, new_name)
+ execute_ddl("ALTER PROPERTY GRAPH #{literal(old_name)} RENAME TO #{literal(new_name)}".freeze)
+ end
+
@@ -806,0 +1591,10 @@
+ # Change the schema for a property graph. Options:
+ # :if_exists :: Use the IF EXISTS clause to not raise an error if the
+ # property graph does not exist.
+ #
+ # DB.set_property_graph_schema(:x, :y)
+ # # ALTER PROPERTY GRAPH x SET SCHEMA y
+ def set_property_graph_schema(old_name, new_name, opts=OPTS)
+ execute_ddl("ALTER PROPERTY GRAPH#{" IF EXISTS" if opts[:if_exists]} #{literal(old_name)} SET SCHEMA #{literal(new_name)}".freeze)
+ end
+
@@ -1206,0 +2001,56 @@
+ # SQL statement for a single ALTER PROPERTY GRAPH operation.
+ def alter_property_graph_op_sql(name, op)
+ sql = String.new << "ALTER PROPERTY GRAPH " << quote_schema_table(name) << " "
+
+ case op_type = op[:op]
+ when :add_vertex_tables
+ sql << "ADD VERTEX TABLES (" <<
+ op[:tables].map do |vertex|
+ create_property_graph_table_sql(vertex) <<
+ create_property_graph_labels_sql(vertex.labels)
+ end.join(', ') << ")"
+ when :add_edge_tables
+ sql << "ADD EDGE TABLES (" <<
+ op[:tables].map do |edge|
+ create_property_graph_table_sql(edge) <<
+ " SOURCE " << create_property_graph_edge_side_sql(edge.source) <<
+ " DESTINATION " << create_property_graph_edge_side_sql(edge.destination) <<
+ create_property_graph_labels_sql(edge.labels)
+ end.join(', ') << ")"
+ when :drop_vertex_tables, :drop_edge_tables
+ sql << (op_type == :drop_vertex_tables ? "DROP VERTEX TABLES " : "DROP EDGE TABLES ") <<
+ literal(op[:aliases])
+ when :add_label
+ sql << alter_property_graph_element_table_sql(op)
+ op[:labels].each do |label_name, properties|
+ sql << " ADD LABEL " << quote_identifier(label_name) <<
+ create_property_graph_properties_clause_sql(properties)
+ end
+ when :drop_label
+ sql << alter_property_graph_element_table_sql(op) <<
+ " DROP LABEL " << quote_identifier(op[:label])
+ when :add_properties
+ sql << alter_property_graph_element_table_sql(op) <<
+ " ALTER LABEL " << quote_identifier(op[:label]) << " ADD PROPERTIES " <<
+ literal(op[:properties])
+ when :drop_properties
+ sql << alter_property_graph_element_table_sql(op) <<
+ " ALTER LABEL " << quote_identifier(op[:label]) << " DROP PROPERTIES " <<
+ literal(op[:properties])
+ else # when :set_owner
+ sql << "OWNER TO " << literal(op[:owner])
+ end
+
+ case op_type
+ when :drop_vertex_tables, :drop_edge_tables, :drop_label, :drop_properties
+ sql << " CASCADE" if op[:cascade]
+ end
+
+ sql
+ end
+
+ # SQL fragment for the ALTER PROPERTY GRAPH ALTER {VERTEX|EDGE} TABLE prefix
+ def alter_property_graph_element_table_sql(op)
+ "ALTER #{op[:kind] == :vertex ? 'VERTEX' : 'EDGE'} TABLE #{quote_identifier(op[:name])}"
+ end
+
@@ -1599,0 +2450,81 @@
+ # SQL statement for creating a property graph.
+ def create_property_graph_sql(name, data, opts=OPTS)
+ sql = String.new
+ sql << "CREATE "
+ sql << "TEMPORARY " if opts[:temp]
+ sql << "PROPERTY GRAPH "
+ sql << quote_schema_table(name)
+
+ unless data.vertices.empty?
+ sql << " VERTEX TABLES ("
+ sql << data.vertices.map do |vertex|
+ create_property_graph_table_sql(vertex) <<
+ create_property_graph_labels_sql(vertex.labels)
+ end.join(', ')
+ sql << ")"
+ end
+
+ unless data.edges.empty?
+ sql << " EDGE TABLES ("
+ sql << data.edges.map do |edge|
+ create_property_graph_table_sql(edge) <<
+ " SOURCE " << create_property_graph_edge_side_sql(edge.source) <<
+ " DESTINATION " << create_property_graph_edge_side_sql(edge.destination) <<
+ create_property_graph_labels_sql(edge.labels)
+ end.join(', ')
+ sql << ")"
+ end
+
+ sql
+ end
+
+ # SQL fragment for the SOURCE or DESTINATION clause of an edge in a property graph.
+ def create_property_graph_edge_side_sql(side)
+ sql = String.new
+ if side.key
+ sql << "KEY " << literal(side.key) << " REFERENCES "
+ end
+ sql << quote_identifier(side.name)
+ if side.references
+ sql << " " << literal(side.references)
+ end
+ sql
+ end
+
+ # SQL fragment for the table name and KEY clause used for vertices and edges in
+ # a property graph.
+ def create_property_graph_table_sql(element)
+ sql = String.new
+ sql << literal(element.name)
+
+ if key = element.key
+ sql << " KEY " << literal(key)
+ end
+
+ sql
+ end
+
+ # SQL fragment for the LABEL/PROPERTIES clauses used for vertices and
+ # edges in a property graph.
+ def create_property_graph_labels_sql(labels)
+ labels.map do |name, properties|
+ sql = String.new
+ sql << " LABEL " << quote_identifier(name) if name
+ sql << create_property_graph_properties_clause_sql(properties)
+ sql
+ end.join
+ end
+
+ # SQL fragment for the NO PROPERTIES, PROPERTIES ALL COLUMNS, or
+ # PROPERTIES (...) clause for a property graph element or label.
+ def create_property_graph_properties_clause_sql(properties)
+ case properties
+ when nil, :all
+ " PROPERTIES ALL COLUMNS"
+ when false, :none, [].freeze
+ " NO PROPERTIES"
+ else
+ " PROPERTIES #{literal(properties)}"
+ end
+ end
+
@@ -1712,0 +2644,5 @@
+ # SQL for dropping a property graph from the database.
+ def drop_property_graph_sql(name, opts=OPTS)
+ "DROP PROPERTY GRAPH#{' IF EXISTS' if opts[:if_exists]} #{literal(name)}#{' CASCADE' if opts[:cascade]}"
+ end
+
@@ -2135,0 +3072,20 @@
+ # Set FOR PORTION OF clause for UPDATE and DELETE statements.
+ # The first argument is the range or multirange column. If two arguments
+ # are provided, the second argument is an expression with the same
+ # database type as the first argument. If three arguments are provided,
+ # the second specifies the inclusive start of the portion to update and the third
+ # specifies the exclusive end of portion to update. When using the three argument
+ # form, nil can be provided as the second or third argument to have the start or
+ # end of the portion be unbounded. Supported on PostgreSQL 19+.
+ # Example:
+ #
+ # DB[:t].for_portion_of(:rc, Sequel.function(:int4range, 1, 2)).update(c: 3)
+ # # UPDATE t FOR PORTION OF rc (int4range(1, 2)) SET c = 3
+ #
+ # DB[:t].for_portion_of(:rc, 1, 2).update(c: 3)
+ # # UPDATE t FOR PORTION OF rc FROM 1 TO 2 SET c = 3
+ def for_portion_of(column, range, to=(arg_not_given=true))
+ range = [range, to].freeze unless arg_not_given
+ clone(:for_portion_of => [column, range].freeze)
+ end
+
@@ -2638 +3594,2 @@
- # Only include the primary table in the main delete clause
+ # Only include the primary table in the main delete clause.
+ # Support FOR PORTION OF.
@@ -2641 +3598 @@
- source_list_append(sql, @opts[:from][0..0])
+ table_for_portion_of_sql_append(sql)
@@ -2725,0 +3683,26 @@
+ # Add FOR PORTION OF SQL if the dataset uses it.
+ def table_for_portion_of_sql_append(sql)
+ fpo_column, fpo_range = @opts[:for_portion_of]
+ if fpo_column
+ table, aliaz = split_alias(@opts[:from].first)
+ source_list_append(sql, [table])
+ sql << ' FOR PORTION OF '
+ literal_append(sql, fpo_column)
+
+ if fpo_range.is_a?(Array)
+ fpo_start, fpo_end = fpo_range
+ sql << ' FROM '
+ literal_append(sql, fpo_start)
+ sql << ' TO '
+ literal_append(sql, fpo_end)
+ else
+ sql << ' ('
+ literal_append(sql, fpo_range)
+ sql << ')'
+ end
+ as_sql_append(sql, aliaz) if aliaz
+ else
+ source_list_append(sql, @opts[:from][0..0])
+ end
+ end
+
@@ -3015 +3998 @@
- # Only include the primary table in the main update clause
+ # Support FOR PORTION OF.
@@ -3018 +4001 @@
- source_list_append(sql, @opts[:from][0..0])
+ table_for_portion_of_sql_append(sql)
lib/sequel/adapters/shared/sqlite.rb
--- /tmp/d20260805-1553-q70zrr/sequel-5.105.0/lib/sequel/adapters/shared/sqlite.rb 2026-08-05 02:35:11.223093377 +0000
+++ /tmp/d20260805-1553-q70zrr/sequel-5.107.0/lib/sequel/adapters/shared/sqlite.rb 2026-08-05 02:35:11.541109872 +0000
@@ -680,3 +680,3 @@
- # Return an array of strings specifying a query explanation for a SELECT of the
- # current dataset. Currently, the options are ignored, but it accepts options
- # to be compatible with other adapters.
+ # Return a string specifying a query explanation for a SELECT of the
+ # current dataset. Options:
+ # :query_plan :: Use EXPLAIN QUERY PLAN instead of EXPLAIN if true.
@@ -687 +687,2 @@
- ds = db.send(:metadata_dataset).clone(:sql=>"EXPLAIN #{select_sql}".freeze)
+ keyword = (opts && opts[:query_plan]) ? "EXPLAIN QUERY PLAN" : "EXPLAIN"
+ ds = db.send(:metadata_dataset).clone(:sql=>"#{keyword} #{select_sql}".freeze)
lib/sequel/database/schema_generator.rb
--- /tmp/d20260805-1553-q70zrr/sequel-5.105.0/lib/sequel/database/schema_generator.rb 2026-08-05 02:35:11.266095607 +0000
+++ /tmp/d20260805-1553-q70zrr/sequel-5.107.0/lib/sequel/database/schema_generator.rb 2026-08-05 02:35:11.553110494 +0000
@@ -20 +20,3 @@
- break loc unless loc.path == __FILE__
+ # Skip core library methods implemented in Ruby
+ path = loc.path
+ break loc unless path == __FILE__ || (path && path.start_with?('<internal:'))
lib/sequel/dataset/sql.rb
--- /tmp/d20260805-1553-q70zrr/sequel-5.105.0/lib/sequel/dataset/sql.rb 2026-08-05 02:35:11.276096126 +0000
+++ /tmp/d20260805-1553-q70zrr/sequel-5.107.0/lib/sequel/dataset/sql.rb 2026-08-05 02:35:11.559110805 +0000
@@ -564,0 +565 @@
+ sql << ' IGNORE NULLS' if window.opts[:ignore_nulls]
lib/sequel/sql.rb
--- /tmp/d20260805-1553-q70zrr/sequel-5.105.0/lib/sequel/sql.rb 2026-08-05 02:35:11.514108471 +0000
+++ /tmp/d20260805-1553-q70zrr/sequel-5.107.0/lib/sequel/sql.rb 2026-08-05 02:35:11.713118794 +0000
@@ -1325,0 +1326 @@
+ ALL = Constant.new(:ALL)
@@ -1994,0 +1996,2 @@
+ # :ignore_nulls :: Can be set to :ignore for IGNORE NULLS (supported on PostgreSQL 19+ for
+ # a subset of default window functions)
lib/sequel/version.rb
--- /tmp/d20260805-1553-q70zrr/sequel-5.105.0/lib/sequel/version.rb 2026-08-05 02:35:11.515108523 +0000
+++ /tmp/d20260805-1553-q70zrr/sequel-5.107.0/lib/sequel/version.rb 2026-08-05 02:35:11.716118949 +0000
@@ -9 +9 @@
- MINOR = 105
+ MINOR = 107 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bumps sequel from 5.105.0 to 5.107.0.
Changelog
Sourced from sequel's changelog.
Commits
09cfd8bBump version to 5.107.0868c461Support :query_plan option in Dataset#explain on SQLitebdb03b6Fix nondeterministic test failure in property graph spec3e45652Add Database#alter_property_graph, #rename_property_graph, and #set_property_...6e118dfAdd Database#property_graphs to return an array of property graph name symbol...114b59cMove parsing check/foreign key constraints to reflection section of PostgreSQ...431979eAdd Database#graph_table to support GRAPH_TABLE expressions on PostgreSQL 19+cef0e9cAdd Database#create_property_graph to support CREATE PROPERTY GRAPH on Postgr...5609197Restore TruffleRuby on CIc08bea8Fix specs on TruffleRubyDependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting
@dependabot rebase.Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
@dependabot rebasewill rebase this PR@dependabot recreatewill recreate this PR, overwriting any edits that have been made to it@dependabot show <dependency name> ignore conditionswill show all of the ignore conditions of the specified dependency@dependabot ignore this major versionwill close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)@dependabot ignore this minor versionwill close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)@dependabot ignore this dependencywill close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)