sequel 5.106.0 → 5.107.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 93dd74cb4407d389f289342898c26327d91db1ddd733478461bd8620a3a1771b
4
- data.tar.gz: b00796ca61d19f2d9aeaaa764565893e73f5dd4c0752e11bd38635fdb94b1cea
3
+ metadata.gz: '06079c7289ee0e9b243cd4b320275936464aa0d02e09831285f622dff88da509'
4
+ data.tar.gz: 80de7cac3306f9b07b21bcd7885bde29da87d9b668488a96024e55002cebeb45
5
5
  SHA512:
6
- metadata.gz: 2502a507d57a7f64dae3ddc9e736254461a3a05f15f95a4835e2ddd83cb3cf3a9f6719f97e6b05239508d4e8445cff8b502198d40eea9f2c0af9d7ea385f829f
7
- data.tar.gz: 8f3e230dc402a4e9d4b2c1d60fb5bdcac580f8368a2ad79a132a29348df8bce6b78f983658c6cf1e406b53164f79f1103cdbfb8ccfd29d0ec90db48a57f348a2
6
+ metadata.gz: 3c42946502241b5d56f7d6c25f39bc7e80e348340283809977a83ba1c66f1372d963cf5aa272452faafd027aee87ea9b4446b9ea31554ac0ab826ccb2fde7528
7
+ data.tar.gz: 9a26c4935906dc87b40d3a4b46c45c78faf51a0f4ca322d113dd34d3495f68540c7fe581f66a5d349cd7aecfd06ead1ae0137031eb9a0b8e146f13d68af4b68c
@@ -256,6 +256,564 @@ module Sequel
256
256
  end
257
257
  end
258
258
 
259
+ module PropertyGraph
260
+ # Base class for all Generator DSL classes. This uses a design where
261
+ # The DSL class is only used for the evaluation of the block, and new
262
+ # returns a frozen struct.
263
+ class Generator
264
+ # Instead of returning the Generator instance, return a frozen struct
265
+ # with data from the generator. This prevents accidentally calling the
266
+ # generator methods, and makes it possible for the generator class and
267
+ # result class to use the same method name in two different ways, with
268
+ # the generator setting data and the frozen struct method returning it.
269
+ # The frozen struct classes use the constant Data under each generator
270
+ # subclass.
271
+ def self.new(*args, &block)
272
+ super(*args, &block).data
273
+ end
274
+
275
+ # Base class for Vertex and Edge.
276
+ class Element < self
277
+ Data = Struct.new(:name, :key, :labels)
278
+
279
+ # +name+ specifies the name of the vertex or edge. It can be an
280
+ # SQL::AliasedExpression to use an alias. Options:
281
+ # :properties :: Specifies fixed properties for the vertex or edge.
282
+ # If this is given, you cannot use the label method
283
+ # inside the block.
284
+ def initialize(name, opts=OPTS, &block)
285
+ @name = name
286
+ @labels = []
287
+ if opts.key?(:properties)
288
+ @labels << [nil, opts[:properties]].freeze
289
+ @labels.freeze
290
+ end
291
+ instance_exec(&block) if block
292
+ @labels.freeze
293
+ freeze
294
+ end
295
+
296
+ def data
297
+ Data.new(@name, @key, @labels).freeze
298
+ end
299
+
300
+ # Set the column(s) to use for the KEY clause, which are the columns
301
+ # that uniquely identify rows in the table:
302
+ #
303
+ # key(:id)
304
+ # # KEY (id)
305
+ #
306
+ # key([:id1, :id2])
307
+ # # KEY (id1, id2)
308
+ def key(columns)
309
+ @key = Array(columns)
310
+ end
311
+
312
+ # Add a label and properties for the label for this vertex/edge.
313
+ # A vertex or edge can have multiple labels with separate properties,
314
+ # if it wasn't created with fixed properties. The +name+ argument
315
+ # specifies the label name. The +properties+ argument specifies the
316
+ # properties:
317
+ # nil, :all :: PROPERTIES ALL COLUMNS
318
+ # false, :none, [] :: NO PROPERTIES
319
+ # Array :: Array of specific properties. Each element should be a Symbol,
320
+ # SQL::Identifier, or SQL::AliasedExpression.
321
+ #
322
+ # label(:label_name)
323
+ # # LABEL label_name PROPERTIES ALL COLUMNS
324
+ #
325
+ # label(:label_name, [])
326
+ # # LABEL label_name NO PROPERTIES
327
+ #
328
+ # label(:label_name, [:c, Sequel[:b].as(:d)], Sequel[:e])
329
+ # # LABEL label_name PROPERTIES (c, b AS d, e)
330
+ def label(name, properties=:all)
331
+ if @labels.frozen?
332
+ raise Error, "cannot specify label for property graph vertex or edge with fixed properties"
333
+ end
334
+ @labels << [name, properties].freeze
335
+ nil
336
+ end
337
+ end
338
+
339
+ # Vertex is used for the block passed to Create#vertex, used to configure
340
+ # vertices in the property graph. It doesn't have any additional behavior
341
+ # compared to the Element class, so this is an alias instead of a subclass.
342
+ Vertex = Element
343
+
344
+ # Target is used for the block passed to Edge#source and Edge#destination,
345
+ # used to configure the source and destination of property graph edges.
346
+ class Target < self
347
+ Data = Struct.new(:name, :key, :references)
348
+
349
+ # +name+ specifies the name of the source or destination.
350
+ def initialize(name, &block)
351
+ @name = name
352
+ @key = nil
353
+ @references = nil
354
+ instance_exec(&block) if block
355
+ freeze
356
+ end
357
+
358
+ def data
359
+ Data.new(@name, @key, @references).freeze
360
+ end
361
+
362
+ # Set the column(s) to use for the KEY clause, which are the columns
363
+ # in the edge table that reference columns in the source or destination.
364
+ # Should be combined with #references to specify the columns being
365
+ # referenced.
366
+ #
367
+ # key(:vertex_id)
368
+ # # KEY (vertex_id)
369
+ #
370
+ # key([:vertex_id1, :vertex_id2])
371
+ # # KEY (vertex_id1, vertex_id2)
372
+ def key(keys)
373
+ @key = Array(keys)
374
+ end
375
+
376
+ # Set the column(s) to use for the REFERENCES clause, which are the columns
377
+ # in the source or destination table that are referenced by the edge table.
378
+ # Should be combined with #key to specify the columns doing the referencing.
379
+ #
380
+ # references(:id)
381
+ # # REFERENCES (id)
382
+ #
383
+ # references([:id1, :id2])
384
+ # # REFERENCES (id1, id2)
385
+ def references(refs)
386
+ @references = Array(refs)
387
+ end
388
+ end
389
+
390
+ # Edge is used for block passed to Create#edge, used to configure edges
391
+ # in the property graph.
392
+ class Edge < Element
393
+ Data = Struct.new(:name, :key, :labels, :source, :destination)
394
+
395
+ # In addition to inherited behavior, raises an error if a block
396
+ # is not passed or source or destination is not called in the block.
397
+ def initialize(name, opts=OPTS, &block)
398
+ super
399
+
400
+ unless @source && @destination
401
+ raise Error, "source and/or destination not defined for property graph edge"
402
+ end
403
+ end
404
+
405
+ def data
406
+ Data.new(@name, @key, @labels, @source, @destination).freeze
407
+ end
408
+
409
+ # Specify the source for the edge, with block evaluted by Target.
410
+ def source(name, &block)
411
+ raise Error, "cannot specify multiple sources for a property graph edge" if @source
412
+ @source = Target.new(name, &block)
413
+ end
414
+
415
+ # Specify the destination for the edge, with block evaluted by Target.
416
+ def destination(name, &block)
417
+ raise Error, "cannot specify multiple destinations for a property graph edge" if @destination
418
+ @destination = Target.new(name, &block)
419
+ end
420
+ end
421
+
422
+ # Create is used to evaluate the block given to DatabaseMethods#create_property_graph,
423
+ # used to specify the vertices and edges in the property graph.
424
+ class Create < self
425
+ Data = Struct.new(:vertices, :edges)
426
+
427
+ def initialize(&block)
428
+ @vertices = []
429
+ @edges = []
430
+ instance_exec(&block)
431
+ @vertices.freeze
432
+ @edges.freeze
433
+ freeze
434
+ end
435
+
436
+ def data
437
+ Data.new(@vertices, @edges).freeze
438
+ end
439
+
440
+ # Adds a vertex to the property graph, with the block evaluted by Vertex.
441
+ def vertex(name, opts=OPTS, &block)
442
+ @vertices << Vertex.new(name, opts, &block)
443
+ end
444
+
445
+ # Adds an edge to the property graph, with the block evaluted by Edge.
446
+ def edge(name, opts=OPTS, &block)
447
+ @edges << Edge.new(name, opts, &block)
448
+ end
449
+ end
450
+
451
+ # AlterElement is used to evaluate the block passed to
452
+ # Alter#alter_vertex_table and Alter#alter_edge_table.
453
+ class AlterElement < self
454
+ # +kind+ is +:vertex+ or +:edge+. +name+ is the alias of the
455
+ # vertex or edge table to alter.
456
+ def initialize(kind, name, &block)
457
+ @kind = kind
458
+ @name = name
459
+ @labels = []
460
+ @operations = []
461
+ instance_exec(&block)
462
+
463
+ # All labels added via #add_label are combined into a single
464
+ # ADD LABEL operation, as PostgreSQL supports adding multiple
465
+ # labels in a single ALTER ... ADD LABEL statement.
466
+ unless @labels.empty?
467
+ @operations << {:op=>:add_label, :kind=>kind, :name=>name, :labels=>@labels.freeze}
468
+ end
469
+
470
+ @operations.each(&:freeze)
471
+ @operations.freeze
472
+ freeze
473
+ end
474
+
475
+ def data
476
+ @operations
477
+ end
478
+
479
+ # Add a label (and optional properties) to the vertex/edge table.
480
+ # Takes the same arguments as Element#label. Can be called multiple
481
+ # times to add multiple labels.
482
+ #
483
+ # add_label(:l)
484
+ # # ADD LABEL l PROPERTIES ALL COLUMNS
485
+ def add_label(name, properties=:all)
486
+ @labels << [name, properties].freeze
487
+ nil
488
+ end
489
+
490
+ # Remove a label from the vertex/edge table. Options:
491
+ # :cascade :: Use CASCADE to drop dependent objects.
492
+ #
493
+ # drop_label(:l)
494
+ # # DROP LABEL l
495
+ def drop_label(name, opts=OPTS)
496
+ @operations << {:op=>:drop_label, :kind=>@kind, :name=>@name, :label=>name, :cascade=>opts[:cascade]}
497
+ nil
498
+ end
499
+
500
+ # Add properties to an existing label on the vertex/edge table.
501
+ # +properties+ is an expression, or array of expressions, the same
502
+ # as the explicit array form of the +properties+ argument to
503
+ # Element#label.
504
+ #
505
+ # add_properties(:l, [:c1, Sequel[:c2].as(:c3)])
506
+ # # ALTER LABEL l ADD PROPERTIES (c1, c2 AS c3)
507
+ def add_properties(label, properties)
508
+ @operations << {:op=>:add_properties, :kind=>@kind, :name=>@name, :label=>label, :properties=>Array(properties)}
509
+ nil
510
+ end
511
+
512
+ # Remove properties from an existing label on the vertex/edge table.
513
+ # +properties+ is a column name, or array of column names. Options:
514
+ # :cascade :: Use CASCADE to drop dependent objects.
515
+ #
516
+ # drop_properties(:l, [:c1])
517
+ # # ALTER LABEL l DROP PROPERTIES (c1)
518
+ def drop_properties(label, properties, opts=OPTS)
519
+ @operations << {:op=>:drop_properties, :kind=>@kind, :name=>@name, :label=>label, :properties=>Array(properties), :cascade=>opts[:cascade]}
520
+ nil
521
+ end
522
+ end
523
+
524
+ # Alter is used to evaluate the block given to DatabaseMethods#alter_property_graph,
525
+ # used to specify changes to an existing property graph.
526
+ class Alter < self
527
+ def initialize(&block)
528
+ @operations = []
529
+ instance_exec(&block)
530
+
531
+ @operations.each do |op|
532
+ case op[:op]
533
+ when :add_vertex_tables, :add_edge_tables
534
+ op[:tables].freeze
535
+ end
536
+ op.freeze
537
+ end
538
+ @operations.freeze
539
+ freeze
540
+ end
541
+
542
+ def data
543
+ @operations
544
+ end
545
+
546
+ # Add a vertex to the property graph, with the block used to configure the
547
+ # vertex.
548
+ #
549
+ # alter_property_graph.add_vertex(:v)
550
+ # # ADD VERTEX TABLES (v)
551
+ def add_vertex(name, opts=OPTS, &block)
552
+ add_tables_operation(:add_vertex_tables) << Vertex.new(name, opts, &block)
553
+ nil
554
+ end
555
+
556
+ # Add an edge to the property graph, with the block used to configure the edge.
557
+ #
558
+ # alter_property_graph.add_edge(:e){source :v1; destination :v2}
559
+ # # ADD EDGE TABLES (e SOURCE v1 DESTINATION v2)
560
+ def add_edge(name, opts=OPTS, &block)
561
+ add_tables_operation(:add_edge_tables) << Edge.new(name, opts, &block)
562
+ nil
563
+ end
564
+
565
+ # Remove vertex tables (referenced by their aliases) from the
566
+ # property graph. +aliases+ can be a single alias or an array.
567
+ # Options:
568
+ # :cascade :: Use CASCADE instead of the default RESTRICT.
569
+ #
570
+ # alter_property_graph.drop_vertex_tables([:v1, :v2])
571
+ # # DROP VERTEX TABLES (v1, v2)
572
+ def drop_vertex_tables(aliases, opts=OPTS)
573
+ @operations << {:op=>:drop_vertex_tables, :aliases=>Array(aliases), :cascade=>opts[:cascade]}
574
+ nil
575
+ end
576
+
577
+ # Remove edge tables (referenced by their aliases) from the property
578
+ # graph. See #drop_vertex_tables.
579
+ #
580
+ # alter_property_graph.drop_edge_tables([:e1, :e2])
581
+ # # DROP EDGE TABLES (e1, e2)
582
+ def drop_edge_tables(aliases, opts=OPTS)
583
+ @operations << {:op=>:drop_edge_tables, :aliases=>Array(aliases), :cascade=>opts[:cascade]}
584
+ nil
585
+ end
586
+
587
+ # Modify an existing vertex table (referenced by its alias).
588
+ #
589
+ # alter_property_graph.alter_vertex_table(:v){add_label :l}
590
+ # # ALTER VERTEX TABLE v ADD LABEL l PROPERTIES ALL COLUMNS
591
+ def alter_vertex_table(name, &block)
592
+ @operations.concat(AlterElement.new(:vertex, name, &block))
593
+ nil
594
+ end
595
+
596
+ # Modify an existing edge table (referenced by its alias).
597
+ #
598
+ # alter_property_graph.alter_edge_table(:e, properties: :none){drop_label :l}
599
+ # # ALTER VERTEX TABLE e DROP LABEL l
600
+ def alter_edge_table(name, &block)
601
+ @operations.concat(AlterElement.new(:edge, name, &block))
602
+ nil
603
+ end
604
+
605
+ # Change the owner of the property graph. +new_owner+ is usually a
606
+ # Symbol or SQL::Identifier for the role name, but can be
607
+ # <tt>Sequel.lit('CURRENT_USER')</tt> or
608
+ # <tt>Sequel.lit('SESSION_USER')</tt>.
609
+ #
610
+ # alter_property_graph.owner_to(:new_owner)
611
+ # # OWNER TO new_owner
612
+ def set_owner(new_owner)
613
+ @operations << {:op=>:set_owner, :owner=>new_owner}
614
+ nil
615
+ end
616
+
617
+ private
618
+
619
+ # Internals of add_vertex and add_edge.
620
+ def add_tables_operation(op_name)
621
+ unless op = @operations.find{|o| o[:op] == op_name}
622
+ @operations << (op = {:op=>op_name, :tables=>[]})
623
+ end
624
+ op[:tables]
625
+ end
626
+ end
627
+ end
628
+
629
+ # Represents a GRAPH_TABLE expression, used to query a property graph
630
+ # via graph pattern matching. This is used in place of a table name
631
+ # expression or dataset in a SELECT query. These are created by calling
632
+ # #graph_table on the related Database object.
633
+ #
634
+ # Table uses a method chaining design, similar to Dataset, where methods
635
+ # return modified frozen copies of the object.
636
+ class Table
637
+ include SQL::AliasMethods
638
+
639
+ # Internal struct for a single element (vertex or edge) in the graph pattern:
640
+ # +type+ :: Either :vertex or :edge.
641
+ # +marker+ :: Connector string to use for the element (empty for initial vertex).
642
+ # +label+ :: Label restriction symbol or SQL::Identifier for the element, if any.
643
+ # Can be an array or set to match multiple labels.
644
+ # +var+ :: Graph pattern variable symbol for the element, if any.
645
+ # +where+ :: WHERE condition for the element, if any.
646
+ Element = Struct.new(:type, :marker, :label, :var, :where) do
647
+ # Method used to create elements, used instead of new
648
+ # to ensure that the returned elements are frozen.
649
+ def self.create(type, marker, label, opts)
650
+ case label
651
+ when Array, Set
652
+ label = label.dup.freeze unless label.frozen?
653
+ end
654
+
655
+ case where = opts[:where]
656
+ when Hash, Array
657
+ where = SQL::BooleanExpression.from_value_pairs(where)
658
+ end
659
+
660
+ new(type, marker, label, opts[:var], where).freeze
661
+ end
662
+
663
+ private_class_method :new
664
+ end
665
+ private_constant :Element
666
+
667
+ # The name of the property graph the table is querying.
668
+ attr_reader :name
669
+
670
+ # A frozen array of Element instances, representing the vertices and
671
+ # edges in the graph pattern.
672
+ attr_reader :elements
673
+
674
+ # A frozen array of the columns used in the COLUMNS clause (aliased
675
+ # as columns_used, as #columns is used to modify the columns).
676
+ attr_reader :columns
677
+ alias columns_used columns
678
+
679
+ # Create a new Table with the given +graph_name+, with +initial_vertex_label+
680
+ # and +initial_vertex_opts+ being used to create the initial vertex.
681
+ # See Table#link for which options are supported for the initial vertex.
682
+ def self.create(graph_name, initial_vertex_label, initial_vertex_opts)
683
+ vertex = Element.create(:vertex, "", initial_vertex_label, initial_vertex_opts)
684
+ new(graph_name, [vertex].freeze, [].freeze)
685
+ end
686
+
687
+ def initialize(name, elements, columns)
688
+ @name = name
689
+ @elements = elements
690
+ @columns = columns
691
+ freeze
692
+ end
693
+
694
+ # Return a modified copy with an element added using a bidirectional link
695
+ # (<tt>-</tt> in the graph pattern).
696
+ # +label+ specifies the label restriction for the element. This can be
697
+ # nil for no label restriction, or an array or set to restrict to the
698
+ # given labels.
699
+ #
700
+ # Options supported:
701
+ # +:var+ :: Specifies a graph pattern variable name for the element,
702
+ # usable in the WHERE or COLUMNS clauses.
703
+ # +:vertex+ :: Specifies that the element being linked to is a vertex.
704
+ # This allows for direct vertex<->vertex linking, instead of
705
+ # the default vertex<->edge<->vertex linking.
706
+ # +:where+ :: An expression to use for the WHERE clause for the element.
707
+ #
708
+ # DB.graph_table(:gn, :v).link(:e)
709
+ # # GRAPH_TABLE (gn MATCH (IS v)-[IS e])
710
+ def link(label, opts=OPTS)
711
+ append_element('-', label, opts)
712
+ end
713
+
714
+ # Similar to #link, but uses a directed link from the previous element
715
+ # to the new element (<tt>-></tt> in the graph pattern). Accepts same
716
+ # arguments and options as #link.
717
+ #
718
+ # DB.graph_table(:gn, :v).to(:e)
719
+ # # GRAPH_TABLE (gn MATCH (IS v)->[IS e])
720
+ def to(label, opts=OPTS)
721
+ append_element('->', label, opts)
722
+ end
723
+
724
+ # Similar to #link, but uses a directed link from the new element
725
+ # to the previous element (<tt><-</tt> in the graph pattern). Accepts
726
+ # same arguments and options as #link.
727
+ #
728
+ # DB.graph_table(:gn, :v).from(:e)
729
+ # # GRAPH_TABLE (gn MATCH (IS v)<-[IS e])
730
+ def from(label, opts=OPTS)
731
+ append_element('<-', label, opts)
732
+ end
733
+
734
+ # Return a modifies copy that uses the given columns. A graph table
735
+ # must have a least one column set before it is used in a query.
736
+ #
737
+ # DB.graph_table(:gn, :v).columns(:a, Sequel[:b].as(:c))
738
+ # # GRAPH_TABLE (gn MATCH (IS v) COLUMNS (a, b AS c))
739
+ def columns(*cols)
740
+ self.class.new(@name, @elements, cols.freeze)
741
+ end
742
+
743
+ # Return a modified copy that adds the given columns to the existing
744
+ # list of columns for the graph table.
745
+ def add_columns(*cols)
746
+ columns(*@columns, *cols)
747
+ end
748
+
749
+ # Append the SQL for the GRAPH_TABLE expression to the given SQL string.
750
+ # Requires graph table have at least one column set.
751
+ def sql_literal_append(ds, sql)
752
+ if @columns.empty?
753
+ raise Error, "cannot use graph_table in a query if it does not return any columns"
754
+ end
755
+ if @elements.last.type == :edge
756
+ raise Error, "cannot use graph_table in a query if the last element is an edge"
757
+ end
758
+
759
+ sql << "GRAPH_TABLE ("
760
+ ds.literal_append(sql, @name)
761
+ sql << " MATCH "
762
+
763
+ @elements.each do |element|
764
+ marker = element.marker
765
+ var = element.var
766
+ label = element.label
767
+ where = element.where
768
+ vertex = element.type == :vertex
769
+
770
+ sql << marker
771
+ sql << (vertex ? '(' : '[')
772
+
773
+ ds.literal_append(sql, var) if var
774
+ if label
775
+ sql << (var ? " IS " : "IS ")
776
+ if label.is_a?(Array)
777
+ label_sep = ""
778
+ label.each do |l|
779
+ sql << label_sep
780
+ label_sep = "|" if label_sep.empty?
781
+ ds.literal_append(sql, l)
782
+ end
783
+ else
784
+ ds.literal_append(sql, label)
785
+ end
786
+ end
787
+
788
+ if where
789
+ sql << ((var || label) ? " WHERE " : "WHERE ")
790
+ ds.literal_append(sql, where)
791
+ end
792
+
793
+ sql << (vertex ? ')' : ']')
794
+ end
795
+
796
+ sql << " COLUMNS "
797
+ ds.literal_append(sql, @columns)
798
+ sql << ")"
799
+ end
800
+
801
+ private
802
+
803
+ # Internals of #link, #to, and #from.
804
+ def append_element(marker, label, opts)
805
+ node_type = if opts[:vertex]
806
+ :vertex
807
+ else
808
+ @elements.last.type == :vertex ? :edge : :vertex
809
+ end
810
+
811
+ element = Element.create(node_type, marker, label, opts)
812
+ self.class.new(@name, (@elements.dup << element).freeze, @columns)
813
+ end
814
+ end
815
+ end
816
+
259
817
  # Error raised when Sequel determines a PostgreSQL exclusion constraint has been violated.
260
818
  class ExclusionConstraintViolation < Sequel::ConstraintViolation; end
261
819
 
@@ -338,6 +896,64 @@ module Sequel
338
896
  add_conversion_proc(oid, block)
339
897
  end
340
898
 
899
+ # Alter the property graph with the given +name+, supported on PostgreSQL 19+.
900
+ # The block uses a DSL, evaluated by PropertyGraph::Generator::Alter. Example:
901
+ #
902
+ # DB.alter_property_graph(:my_graph) do
903
+ # # PropertyGraph::Generator::Alter
904
+ # add_vertex :companies2
905
+ # # ALTER PROPERTY GRAPH "my_graph" ADD VERTEX TABLES ("companies2")
906
+ #
907
+ # add_edge :works_at2 do
908
+ # # PropertyGraph::Generator::Edge
909
+ # source :people
910
+ # destination :companies2
911
+ # end
912
+ # # ALTER PROPERTY GRAPH "my_graph" ADD EDGE TABLES
913
+ # # ("works_at2" SOURCE "people" DESTINATION "companies2")
914
+ #
915
+ # drop_vertex_tables [:p2], cascade: true
916
+ # # ALTER PROPERTY GRAPH "my_graph" DROP VERTEX TABLES ("p2") CASCADE
917
+ #
918
+ # drop_edge_tables :e2
919
+ # # ALTER PROPERTY GRAPH "my_graph" DROP EDGE TABLES ("e2")
920
+ #
921
+ # alter_vertex_table :companies do
922
+ # # PropertyGraph::Generator::AlterElement
923
+ # add_label :public_company, [:name, :symbol]
924
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER VERTEX TABLE "companies"
925
+ # # ADD LABEL "public_company" PROPERTIES ("name", "symbol")
926
+ #
927
+ # drop_label :private_company
928
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER VERTEX TABLE "companies"
929
+ # # DROP LABEL "private_company"
930
+ #
931
+ # add_properties :company, :revenue
932
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER VERTEX TABLE "companies"
933
+ # # ALTER LABEL "company" ADD PROPERTIES ("revenue")
934
+ #
935
+ # drop_properties :company, :internal_id, cascade: true
936
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER VERTEX TABLE "companies"
937
+ # # ALTER LABEL "company" DROP PROPERTIES ("internal_id") CASCADE
938
+ # end
939
+ #
940
+ # alter_edge_table :works_at do
941
+ # # PropertyGraph::Generator::AlterElement
942
+ # add_label :employment
943
+ # end
944
+ # # ALTER PROPERTY GRAPH "my_graph" ALTER EDGE TABLE "works_at"
945
+ # # ADD LABEL "employment" PROPERTIES ALL COLUMNS
946
+ #
947
+ # owner_to :new_owner
948
+ # # ALTER PROPERTY GRAPH "my_graph" OWNER TO "new_owner"
949
+ # end
950
+ def alter_property_graph(name, &block)
951
+ PropertyGraph::Generator::Alter.new(&block).each do |op|
952
+ execute_ddl(alter_property_graph_op_sql(name, op).freeze)
953
+ end
954
+ nil
955
+ end
956
+
341
957
  def commit_prepared_transaction(transaction_id, opts=OPTS)
342
958
  run("COMMIT PREPARED #{literal(transaction_id)}".freeze, opts)
343
959
  end
@@ -467,6 +1083,67 @@ module Sequel
467
1083
  self << create_language_sql(name, opts).freeze
468
1084
  end
469
1085
 
1086
+ # Create a property graph in the database, supported on PostgreSQL 19+.
1087
+ #
1088
+ # Arguments:
1089
+ # name :: Name of the property graph
1090
+ # opts :: options hash:
1091
+ # :temp :: Create the property graph as a temporary property graph.
1092
+ #
1093
+ # The block uses a DSL, with classes under PropertyGraph::Generator:
1094
+ #
1095
+ # DB.create_property_graph(:my_graph) do
1096
+ # # PropertyGraph::Generator::Create
1097
+ # vertex :people
1098
+ #
1099
+ # vertex Sequel.as(:people, :p), properties: []
1100
+ #
1101
+ # vertex Sequel.as(:companies, :c) do
1102
+ # # PropertyGraph::Generator::Vertex
1103
+ # key :id
1104
+ # label :company
1105
+ # label :c, [:name, (Sequel[:revenue] / 1000).as(:revenue_thousands)]
1106
+ # end
1107
+ #
1108
+ # edge :works_at do
1109
+ # # PropertyGraph::Generator::Edge
1110
+ # source :people
1111
+ # destination :c
1112
+ # end
1113
+ #
1114
+ # edge Sequel.as(:employment, :e) do
1115
+ # source :people do
1116
+ # # PropertyGraph::Generator::Target
1117
+ # key :person_id
1118
+ # references :id
1119
+ # end
1120
+ # destination :c do
1121
+ # # PropertyGraph::Generator::Target
1122
+ # key :company_id
1123
+ # references :id
1124
+ # end
1125
+ # label :employment
1126
+ # end
1127
+ # end
1128
+ # # CREATE PROPERTY GRAPH "my_graph"
1129
+ # # VERTEX TABLES (
1130
+ # # "people",
1131
+ # # "people" AS "p" NO PROPERTIES,
1132
+ # # "companies" AS "c" KEY ("id")
1133
+ # # LABEL "company" PROPERTIES ALL COLUMNS
1134
+ # # LABEL "c" PROPERTIES ("name", ("revenue" / 1000) AS "revenue_thousands"))
1135
+ # # EDGE TABLES (
1136
+ # # "works_at"
1137
+ # # SOURCE "people"
1138
+ # # DESTINATION "c",
1139
+ # # "employment" AS "e"
1140
+ # # SOURCE KEY ("person_id") REFERENCES "people" ("id")
1141
+ # # DESTINATION KEY ("company_id") REFERENCES "c" ("id")
1142
+ # # LABEL "employment" PROPERTIES ALL COLUMNS)
1143
+ def create_property_graph(name, opts=OPTS, &block)
1144
+ execute_ddl(create_property_graph_sql(name, PropertyGraph::Generator::Create.new(&block), opts))
1145
+ end
1146
+
470
1147
  # Create a schema in the database. Arguments:
471
1148
  # name :: Name of the schema (e.g. admin)
472
1149
  # opts :: options hash:
@@ -564,6 +1241,15 @@ module Sequel
564
1241
  self << drop_language_sql(name, opts).freeze
565
1242
  end
566
1243
 
1244
+ # Drops a property graph from the database. Arguments:
1245
+ # name :: name of the property graph to drop
1246
+ # opts :: options hash:
1247
+ # :cascade :: Drop other objects depending on this property_graph.
1248
+ # :if_exists :: Don't raise an error if the property graph doesn't exist.
1249
+ def drop_property_graph(name, opts=OPTS)
1250
+ self << drop_property_graph_sql(name, opts).freeze
1251
+ end
1252
+
567
1253
  # Drops a schema from the database. Arguments:
568
1254
  # name :: name of the schema to drop
569
1255
  # opts :: options hash:
@@ -651,6 +1337,82 @@ module Sequel
651
1337
  super
652
1338
  end
653
1339
 
1340
+ # Return a PropertyGraph::Table instance for a property graph search
1341
+ # (a GRAPH_TABLE clause for a SELECT query). Supported on PostgreSQL 19+.
1342
+ #
1343
+ # Arguments:
1344
+ # +property_graph_name+ :: The property graph to query
1345
+ # +initial_vertex_label+ :: The label restriction for the initial vertex for the
1346
+ # graph pattern (can be nil for no label, or an array
1347
+ # or set for restricting to one of multiple labels).
1348
+ # +initial_vertex_opts+ :: The options for the initial vertex, see
1349
+ # PropertyGraph::Table#link for available options.
1350
+ #
1351
+ # The returned instance should be further modified by calling methods on it,
1352
+ # using a similar approach to how datasets work, where the methods return a
1353
+ # modified copy of the receiver. The available methods:
1354
+ #
1355
+ # link :: Add a bidirectional link to a new element (vertex or edge)
1356
+ # to :: Add a directional link from the last element to the new element
1357
+ # from :: Add a direciton link from the new element to last element
1358
+ # columns :: Replace the columns the graph table returns
1359
+ # add_columns :: Append to the columns the graph table returns.
1360
+ #
1361
+ # See PropertyGraph::Table for the details of these methods and the arguments
1362
+ # and options they support. Note that for a graph table to be usable in a query,
1363
+ # it must return at least one column, and the last element in the graph pattern
1364
+ # must be a vertex.
1365
+ #
1366
+ # gt = DB.graph_table(:pgn, :iv)
1367
+ # # Not yet usable, does not return any columns
1368
+ #
1369
+ # # Set columns for graph table
1370
+ # gt = gt.columns(:c, Sequel[1].as(:d))
1371
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv") COLUMNS ("c", 1 AS "d"))
1372
+ #
1373
+ # # Adds directional link to edge, since last (initial) element was a vertex
1374
+ # gt = gt.link(:e1)
1375
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"] COLUMNS ("c", 1 AS "d"))
1376
+ #
1377
+ # # Adds directional link from edge to vertex, since last element was an edge
1378
+ # gt = gt.to(:v2)
1379
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2") COLUMNS ("c", 1 AS "d"))
1380
+ #
1381
+ # # Adds bidirection link from vertex to vertex (overriding the default)
1382
+ # gt = gt.link(:v3, vertex: true)
1383
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3") COLUMNS ("c", 1 AS "d"))
1384
+ #
1385
+ # # Adds directional link from new edge to last vertex, since last element was an vertex.
1386
+ # # Sets graph pattern variable name and uses it in a WHERE clause for the added element.
1387
+ # gt = gt.from(:e2, var: :a2, where: {Sequel[:a2][:c] => 1})
1388
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3")
1389
+ # # <-["a2" IS "e2" WHERE ("a2"."c" = 1)] COLUMNS ("c", 1 AS "d"))
1390
+ #
1391
+ # # Can use nil as a label for no label restriction, both with and without a variable name
1392
+ # gt = gt.to(nil).to(nil, var: :a3)
1393
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3")
1394
+ # # <-["a2" IS "e2" WHERE ("a2"."c" = 1)]->[]->("a3") COLUMNS ("c", 1 AS "d"))
1395
+ #
1396
+ # # Can restrict to a one of a set of labels
1397
+ # gt = gt.from([:x, :y], var: :a6)
1398
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3")
1399
+ # # <-["a2" IS "e2" WHERE ("a2"."c" = 1)]->[]->("a3")->["a6" IS "x"|"y"] COLUMNS ("c", 1 AS "d"))
1400
+ #
1401
+ # # Add column(s) to the graph table
1402
+ # gt = gt.add_columns(:y)
1403
+ # # GRAPH_TABLE ("pgn" MATCH (IS "iv")-[IS "e1"]->(IS "v2")-(IS "v3")
1404
+ # # <-["a2" IS "e2" WHERE ("a2"."c" = 1)]->[]->("a3")->["a6" IS "x"|"y"]
1405
+ # # COLUMNS ("c", 1 AS "d", "y"))
1406
+ #
1407
+ # DB.from(gt)
1408
+ # # SELECT * FROM GRAPH_TABLE (...)
1409
+ #
1410
+ # DB.from(:x).cross_join(gt)
1411
+ # # SELECT * FROM "x" CROSS JOIN GRAPH_TABLE (...)
1412
+ def graph_table(property_graph_name, initial_vertex_label, initial_vertex_opts=OPTS)
1413
+ PropertyGraph::Table.create(property_graph_name, initial_vertex_label, initial_vertex_opts)
1414
+ end
1415
+
654
1416
  # Immediately apply deferrable constraints.
655
1417
  #
656
1418
  # :constraints :: An identifier of the constraint, or an array of
@@ -741,6 +1503,28 @@ module Sequel
741
1503
  Sequel.synchronize{@primary_key_sequences[quoted_table] = value} if value
742
1504
  end
743
1505
 
1506
+ # Array of symbols specifying property graphs in the current database.
1507
+ # The dataset used is yielded to the block if one is provided,
1508
+ # otherwise, an array of symbols of property graph names is returned.
1509
+ # Supported on PostgreSQL 19+, will be an empty array on lower versions.
1510
+ #
1511
+ # Options:
1512
+ # :qualify :: Return the property graph names as Sequel::SQL::QualifiedIdentifier
1513
+ # instances, using the schema the property graph is located in as the qualifier.
1514
+ # :schema :: The schema to search
1515
+ # :server :: The server to use
1516
+ def property_graphs(opts=OPTS, &block)
1517
+ pg_class_relname('g', opts, &block)
1518
+ end
1519
+
1520
+ # Rename a property graph.
1521
+ #
1522
+ # DB.rename_property_graph(:x, :y)
1523
+ # # ALTER PROPERTY GRAPH x RENAME TO y
1524
+ def rename_property_graph(old_name, new_name)
1525
+ execute_ddl("ALTER PROPERTY GRAPH #{literal(old_name)} RENAME TO #{literal(new_name)}".freeze)
1526
+ end
1527
+
744
1528
  # Rename a schema in the database. Arguments:
745
1529
  # name :: Current name of the schema
746
1530
  # opts :: New name for the schema
@@ -804,6 +1588,16 @@ module Sequel
804
1588
  @server_version = swallow_database_error{ds.with_sql("SELECT CAST(current_setting('server_version_num') AS integer) AS v").single_value} || 0
805
1589
  end
806
1590
 
1591
+ # Change the schema for a property graph. Options:
1592
+ # :if_exists :: Use the IF EXISTS clause to not raise an error if the
1593
+ # property graph does not exist.
1594
+ #
1595
+ # DB.set_property_graph_schema(:x, :y)
1596
+ # # ALTER PROPERTY GRAPH x SET SCHEMA y
1597
+ def set_property_graph_schema(old_name, new_name, opts=OPTS)
1598
+ execute_ddl("ALTER PROPERTY GRAPH#{" IF EXISTS" if opts[:if_exists]} #{literal(old_name)} SET SCHEMA #{literal(new_name)}".freeze)
1599
+ end
1600
+
807
1601
  # PostgreSQL supports CREATE TABLE IF NOT EXISTS on 9.1+
808
1602
  def supports_create_table_if_not_exists?
809
1603
  server_version >= 90100
@@ -1204,6 +1998,62 @@ module Sequel
1204
1998
  raise e unless /canceling statement due to (?:statement|lock) timeout/ =~ e.message
1205
1999
  end
1206
2000
 
2001
+ # SQL statement for a single ALTER PROPERTY GRAPH operation.
2002
+ def alter_property_graph_op_sql(name, op)
2003
+ sql = String.new << "ALTER PROPERTY GRAPH " << quote_schema_table(name) << " "
2004
+
2005
+ case op_type = op[:op]
2006
+ when :add_vertex_tables
2007
+ sql << "ADD VERTEX TABLES (" <<
2008
+ op[:tables].map do |vertex|
2009
+ create_property_graph_table_sql(vertex) <<
2010
+ create_property_graph_labels_sql(vertex.labels)
2011
+ end.join(', ') << ")"
2012
+ when :add_edge_tables
2013
+ sql << "ADD EDGE TABLES (" <<
2014
+ op[:tables].map do |edge|
2015
+ create_property_graph_table_sql(edge) <<
2016
+ " SOURCE " << create_property_graph_edge_side_sql(edge.source) <<
2017
+ " DESTINATION " << create_property_graph_edge_side_sql(edge.destination) <<
2018
+ create_property_graph_labels_sql(edge.labels)
2019
+ end.join(', ') << ")"
2020
+ when :drop_vertex_tables, :drop_edge_tables
2021
+ sql << (op_type == :drop_vertex_tables ? "DROP VERTEX TABLES " : "DROP EDGE TABLES ") <<
2022
+ literal(op[:aliases])
2023
+ when :add_label
2024
+ sql << alter_property_graph_element_table_sql(op)
2025
+ op[:labels].each do |label_name, properties|
2026
+ sql << " ADD LABEL " << quote_identifier(label_name) <<
2027
+ create_property_graph_properties_clause_sql(properties)
2028
+ end
2029
+ when :drop_label
2030
+ sql << alter_property_graph_element_table_sql(op) <<
2031
+ " DROP LABEL " << quote_identifier(op[:label])
2032
+ when :add_properties
2033
+ sql << alter_property_graph_element_table_sql(op) <<
2034
+ " ALTER LABEL " << quote_identifier(op[:label]) << " ADD PROPERTIES " <<
2035
+ literal(op[:properties])
2036
+ when :drop_properties
2037
+ sql << alter_property_graph_element_table_sql(op) <<
2038
+ " ALTER LABEL " << quote_identifier(op[:label]) << " DROP PROPERTIES " <<
2039
+ literal(op[:properties])
2040
+ else # when :set_owner
2041
+ sql << "OWNER TO " << literal(op[:owner])
2042
+ end
2043
+
2044
+ case op_type
2045
+ when :drop_vertex_tables, :drop_edge_tables, :drop_label, :drop_properties
2046
+ sql << " CASCADE" if op[:cascade]
2047
+ end
2048
+
2049
+ sql
2050
+ end
2051
+
2052
+ # SQL fragment for the ALTER PROPERTY GRAPH ALTER {VERTEX|EDGE} TABLE prefix
2053
+ def alter_property_graph_element_table_sql(op)
2054
+ "ALTER #{op[:kind] == :vertex ? 'VERTEX' : 'EDGE'} TABLE #{quote_identifier(op[:name])}"
2055
+ end
2056
+
1207
2057
  def alter_table_add_column_sql(table, op)
1208
2058
  "ADD COLUMN#{' IF NOT EXISTS' if op[:if_not_exists]} #{column_definition_sql(op)}"
1209
2059
  end
@@ -1597,6 +2447,87 @@ module Sequel
1597
2447
  sql
1598
2448
  end
1599
2449
 
2450
+ # SQL statement for creating a property graph.
2451
+ def create_property_graph_sql(name, data, opts=OPTS)
2452
+ sql = String.new
2453
+ sql << "CREATE "
2454
+ sql << "TEMPORARY " if opts[:temp]
2455
+ sql << "PROPERTY GRAPH "
2456
+ sql << quote_schema_table(name)
2457
+
2458
+ unless data.vertices.empty?
2459
+ sql << " VERTEX TABLES ("
2460
+ sql << data.vertices.map do |vertex|
2461
+ create_property_graph_table_sql(vertex) <<
2462
+ create_property_graph_labels_sql(vertex.labels)
2463
+ end.join(', ')
2464
+ sql << ")"
2465
+ end
2466
+
2467
+ unless data.edges.empty?
2468
+ sql << " EDGE TABLES ("
2469
+ sql << data.edges.map do |edge|
2470
+ create_property_graph_table_sql(edge) <<
2471
+ " SOURCE " << create_property_graph_edge_side_sql(edge.source) <<
2472
+ " DESTINATION " << create_property_graph_edge_side_sql(edge.destination) <<
2473
+ create_property_graph_labels_sql(edge.labels)
2474
+ end.join(', ')
2475
+ sql << ")"
2476
+ end
2477
+
2478
+ sql
2479
+ end
2480
+
2481
+ # SQL fragment for the SOURCE or DESTINATION clause of an edge in a property graph.
2482
+ def create_property_graph_edge_side_sql(side)
2483
+ sql = String.new
2484
+ if side.key
2485
+ sql << "KEY " << literal(side.key) << " REFERENCES "
2486
+ end
2487
+ sql << quote_identifier(side.name)
2488
+ if side.references
2489
+ sql << " " << literal(side.references)
2490
+ end
2491
+ sql
2492
+ end
2493
+
2494
+ # SQL fragment for the table name and KEY clause used for vertices and edges in
2495
+ # a property graph.
2496
+ def create_property_graph_table_sql(element)
2497
+ sql = String.new
2498
+ sql << literal(element.name)
2499
+
2500
+ if key = element.key
2501
+ sql << " KEY " << literal(key)
2502
+ end
2503
+
2504
+ sql
2505
+ end
2506
+
2507
+ # SQL fragment for the LABEL/PROPERTIES clauses used for vertices and
2508
+ # edges in a property graph.
2509
+ def create_property_graph_labels_sql(labels)
2510
+ labels.map do |name, properties|
2511
+ sql = String.new
2512
+ sql << " LABEL " << quote_identifier(name) if name
2513
+ sql << create_property_graph_properties_clause_sql(properties)
2514
+ sql
2515
+ end.join
2516
+ end
2517
+
2518
+ # SQL fragment for the NO PROPERTIES, PROPERTIES ALL COLUMNS, or
2519
+ # PROPERTIES (...) clause for a property graph element or label.
2520
+ def create_property_graph_properties_clause_sql(properties)
2521
+ case properties
2522
+ when nil, :all
2523
+ " PROPERTIES ALL COLUMNS"
2524
+ when false, :none, [].freeze
2525
+ " NO PROPERTIES"
2526
+ else
2527
+ " PROPERTIES #{literal(properties)}"
2528
+ end
2529
+ end
2530
+
1600
2531
  # SQL for creating a schema.
1601
2532
  def create_schema_sql(name, opts=OPTS)
1602
2533
  "CREATE SCHEMA #{'IF NOT EXISTS ' if opts[:if_not_exists]}#{quote_identifier(name)}#{" AUTHORIZATION #{literal(opts[:owner])}" if opts[:owner]}"
@@ -1710,6 +2641,11 @@ module Sequel
1710
2641
  "DROP LANGUAGE#{' IF EXISTS' if opts[:if_exists]} #{name}#{' CASCADE' if opts[:cascade]}"
1711
2642
  end
1712
2643
 
2644
+ # SQL for dropping a property graph from the database.
2645
+ def drop_property_graph_sql(name, opts=OPTS)
2646
+ "DROP PROPERTY GRAPH#{' IF EXISTS' if opts[:if_exists]} #{literal(name)}#{' CASCADE' if opts[:cascade]}"
2647
+ end
2648
+
1713
2649
  # SQL for dropping a schema from the database.
1714
2650
  def drop_schema_sql(name, opts=OPTS)
1715
2651
  "DROP SCHEMA#{' IF EXISTS' if opts[:if_exists]} #{quote_identifier(name)}#{' CASCADE' if opts[:cascade]}"
@@ -677,14 +677,15 @@ module Sequel
677
677
  super
678
678
  end
679
679
 
680
- # Return an array of strings specifying a query explanation for a SELECT of the
681
- # current dataset. Currently, the options are ignored, but it accepts options
682
- # to be compatible with other adapters.
680
+ # Return a string specifying a query explanation for a SELECT of the
681
+ # current dataset. Options:
682
+ # :query_plan :: Use EXPLAIN QUERY PLAN instead of EXPLAIN if true.
683
683
  def explain(opts=nil)
684
684
  # Load the PrettyTable class, needed for explain output
685
685
  Sequel.extension(:_pretty_table) unless defined?(Sequel::PrettyTable)
686
686
 
687
- ds = db.send(:metadata_dataset).clone(:sql=>"EXPLAIN #{select_sql}".freeze)
687
+ keyword = (opts && opts[:query_plan]) ? "EXPLAIN QUERY PLAN" : "EXPLAIN"
688
+ ds = db.send(:metadata_dataset).clone(:sql=>"#{keyword} #{select_sql}".freeze)
688
689
  rows = ds.all
689
690
  Sequel::PrettyTable.string(rows, ds.columns)
690
691
  end
@@ -17,7 +17,9 @@ module Sequel
17
17
  if RUBY_VERSION >= "3.2"
18
18
  # :nocov:
19
19
  caller_loc = Thread.each_caller_location do |loc|
20
- break loc unless loc.path == __FILE__
20
+ # Skip core library methods implemented in Ruby
21
+ path = loc.path
22
+ break loc unless path == __FILE__ || (path && path.start_with?('<internal:'))
21
23
  end
22
24
  caller_loc &&= "#{caller_loc.path}:#{caller_loc.lineno}: "
23
25
  end
@@ -6,7 +6,7 @@ module Sequel
6
6
 
7
7
  # The minor version of Sequel. Bumped for every non-patch level
8
8
  # release, generally around once a month.
9
- MINOR = 106
9
+ MINOR = 107
10
10
 
11
11
  # The tiny version of Sequel. Usually 0, only bumped for bugfix
12
12
  # releases that fix regressions from previous versions.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sequel
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.106.0
4
+ version: 5.107.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
@@ -455,7 +455,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
455
455
  - !ruby/object:Gem::Version
456
456
  version: '0'
457
457
  requirements: []
458
- rubygems_version: 4.0.10
458
+ rubygems_version: 4.0.16
459
459
  specification_version: 4
460
460
  summary: The Database Toolkit for Ruby
461
461
  test_files: []