github.com/cayleygraph/cayley@v0.7.7/graph/sql/shape_test.go (about)

     1  package sql
     2  
     3  import (
     4  	"fmt"
     5  	"testing"
     6  
     7  	"github.com/cayleygraph/cayley/graph"
     8  	"github.com/cayleygraph/cayley/graph/iterator"
     9  	"github.com/cayleygraph/cayley/graph/shape"
    10  	"github.com/cayleygraph/quad"
    11  	"github.com/stretchr/testify/require"
    12  )
    13  
    14  type stringVal string
    15  
    16  func (s stringVal) Key() interface{} {
    17  	return string(s)
    18  }
    19  
    20  func (s stringVal) SQLValue() interface{} {
    21  	return string(s)
    22  }
    23  
    24  func sVal(s string) stringVal {
    25  	return stringVal(s)
    26  }
    27  
    28  func sVals(arr ...string) []Value {
    29  	out := make([]Value, 0, len(arr))
    30  	for _, s := range arr {
    31  		out = append(out, sVal(s))
    32  	}
    33  	return out
    34  }
    35  
    36  var shapeCases = []struct {
    37  	skip bool
    38  	name string
    39  	s    shape.Shape
    40  	qu   string
    41  	args []Value
    42  }{
    43  	{
    44  		name: "all nodes",
    45  		s:    shape.AllNodes{},
    46  		qu:   `SELECT hash AS ` + tagNode + ` FROM nodes`,
    47  	},
    48  	{
    49  		name: "lookup iri",
    50  		s:    shape.Lookup{quad.IRI("a")},
    51  		qu:   `SELECT hash AS ` + tagNode + ` FROM nodes WHERE hash = $1`,
    52  		args: []Value{HashOf(quad.IRI("a"))},
    53  	},
    54  	{
    55  		name: "gt iri",
    56  		s: shape.Filter{
    57  			From: shape.AllNodes{},
    58  			Filters: []shape.ValueFilter{
    59  				shape.Comparison{Op: iterator.CompareGT, Val: quad.IRI("a")},
    60  			},
    61  		},
    62  		qu:   `SELECT hash AS ` + tagNode + ` FROM nodes WHERE value_string > $1 AND iri IS true`,
    63  		args: []Value{StringVal("a")},
    64  	},
    65  	{
    66  		name: "gt string",
    67  		s: shape.Filter{
    68  			From: shape.AllNodes{},
    69  			Filters: []shape.ValueFilter{
    70  				shape.Comparison{Op: iterator.CompareGT, Val: quad.String("a")},
    71  			},
    72  		},
    73  		qu:   `SELECT hash AS ` + tagNode + ` FROM nodes WHERE value_string > $1 AND iri IS NULL AND bnode IS NULL AND datatype IS NULL AND language IS NULL`,
    74  		args: []Value{StringVal("a")},
    75  	},
    76  	{
    77  		name: "gt typed string",
    78  		s: shape.Filter{
    79  			From: shape.AllNodes{},
    80  			Filters: []shape.ValueFilter{
    81  				shape.Comparison{Op: iterator.CompareGT, Val: quad.TypedString{Value: "a", Type: "A"}},
    82  			},
    83  		},
    84  		qu:   `SELECT hash AS ` + tagNode + ` FROM nodes WHERE value_string > $1 AND datatype = $2`,
    85  		args: []Value{StringVal("a"), StringVal("A")},
    86  	},
    87  	{
    88  		name: "lookup int",
    89  		s: shape.Filter{
    90  			From: shape.AllNodes{},
    91  			Filters: []shape.ValueFilter{
    92  				shape.Comparison{Op: iterator.CompareGT, Val: quad.Int(42)},
    93  			},
    94  		},
    95  		qu:   `SELECT hash AS ` + tagNode + ` FROM nodes WHERE value_int > $1`,
    96  		args: []Value{IntVal(42)},
    97  	},
    98  	{
    99  		name: "all quads",
   100  		s:    shape.Quads{},
   101  		qu: `SELECT t_1.subject_hash AS __subject, t_1.predicate_hash AS __predicate, t_1.object_hash AS __object, t_1.label_hash AS __label
   102  	FROM quads AS t_1`,
   103  	},
   104  	{
   105  		name: "limit quads and skip first",
   106  		s:    shape.Page{From: shape.Quads{}, Limit: 100, Skip: 1},
   107  		qu: `SELECT t_1.subject_hash AS __subject, t_1.predicate_hash AS __predicate, t_1.object_hash AS __object, t_1.label_hash AS __label
   108  	FROM quads AS t_1
   109  	LIMIT 100
   110  	OFFSET 1`,
   111  	},
   112  	{
   113  		name: "quads with subject and predicate",
   114  		s: shape.Quads{
   115  			{Dir: quad.Subject, Values: shape.Fixed{sVal("s")}},
   116  			{Dir: quad.Predicate, Values: shape.Fixed{sVal("p")}},
   117  		},
   118  		qu: `SELECT t_1.subject_hash AS __subject, t_1.predicate_hash AS __predicate, t_1.object_hash AS __object, t_1.label_hash AS __label
   119  	FROM quads AS t_1
   120  	WHERE t_1.subject_hash = $1 AND t_1.predicate_hash = $2`,
   121  		args: sVals("s", "p"),
   122  	},
   123  	{
   124  		name: "quad actions",
   125  		s: shape.QuadsAction{
   126  			Result: quad.Subject,
   127  			Save: map[quad.Direction][]string{
   128  				quad.Object: {"o1", "o2"},
   129  				quad.Label:  {"l 1"},
   130  			},
   131  			Filter: map[quad.Direction]graph.Ref{
   132  				quad.Predicate: sVal("p"),
   133  			},
   134  		},
   135  		qu: `SELECT subject_hash AS ` + tagNode + `, object_hash AS o1, object_hash AS o2, label_hash AS "l 1"
   136  	FROM quads
   137  	WHERE predicate_hash = $1`,
   138  		args: sVals("p"),
   139  	},
   140  	{
   141  		name: "quad actions and save",
   142  		s: shape.Save{
   143  			Tags: []string{"sub"},
   144  			From: shape.QuadsAction{
   145  				Result: quad.Subject,
   146  				Save: map[quad.Direction][]string{
   147  					quad.Object: {"o1", "o2"},
   148  					quad.Label:  {"l 1"},
   149  				},
   150  				Filter: map[quad.Direction]graph.Ref{
   151  					quad.Predicate: sVal("p"),
   152  				},
   153  			},
   154  		},
   155  		qu: `SELECT subject_hash AS sub, subject_hash AS ` + tagNode + `, object_hash AS o1, object_hash AS o2, label_hash AS "l 1"
   156  	FROM quads
   157  	WHERE predicate_hash = $1`,
   158  		args: sVals("p"),
   159  	},
   160  	{
   161  		name: "quads with subquery",
   162  		s: shape.Quads{
   163  			{Dir: quad.Subject, Values: shape.Fixed{sVal("s")}},
   164  			{
   165  				Dir: quad.Predicate,
   166  				Values: shape.QuadsAction{
   167  					Result: quad.Subject,
   168  					Filter: map[quad.Direction]graph.Ref{
   169  						quad.Predicate: sVal("p"),
   170  					},
   171  				},
   172  			},
   173  		},
   174  		qu: `SELECT t_1.subject_hash AS __subject, t_1.predicate_hash AS __predicate, t_1.object_hash AS __object, t_1.label_hash AS __label
   175  	FROM quads AS t_1, (SELECT subject_hash AS ` + tagNode + ` FROM quads WHERE predicate_hash = $1) AS t_2
   176  	WHERE t_1.subject_hash = $2 AND t_1.predicate_hash = t_2.` + tagNode,
   177  		args: sVals("p", "s"),
   178  	},
   179  	{
   180  		name: "quads with subquery (inner tags)",
   181  		s: shape.Quads{
   182  			{Dir: quad.Subject, Values: shape.Fixed{sVal("s")}},
   183  			{
   184  				Dir: quad.Predicate,
   185  				Values: shape.Save{
   186  					Tags: []string{"pred"},
   187  					From: shape.QuadsAction{
   188  						Result: quad.Subject,
   189  						Save: map[quad.Direction][]string{
   190  							quad.Object: {"ob"},
   191  						},
   192  						Filter: map[quad.Direction]graph.Ref{
   193  							quad.Predicate: sVal("p"),
   194  						},
   195  					},
   196  				},
   197  			},
   198  		},
   199  		qu: `SELECT t_1.subject_hash AS __subject, t_1.predicate_hash AS __predicate, t_1.object_hash AS __object, t_1.label_hash AS __label, t_2.subject_hash AS pred, t_2.object_hash AS ob
   200  	FROM quads AS t_1, quads AS t_2
   201  	WHERE t_1.subject_hash = $1 AND t_2.predicate_hash = $2 AND t_1.predicate_hash = t_2.subject_hash`,
   202  		args: sVals("s", "p"),
   203  	},
   204  	{
   205  		name: "quads with subquery (limit)",
   206  		s: shape.Quads{
   207  			{Dir: quad.Subject, Values: shape.Fixed{sVal("s")}},
   208  			{
   209  				Dir: quad.Predicate,
   210  				Values: shape.Page{
   211  					Limit: 10,
   212  					From: shape.QuadsAction{
   213  						Result: quad.Subject,
   214  						Filter: map[quad.Direction]graph.Ref{
   215  							quad.Predicate: sVal("p"),
   216  						},
   217  					},
   218  				},
   219  			},
   220  		},
   221  		qu: `SELECT t_1.subject_hash AS __subject, t_1.predicate_hash AS __predicate, t_1.object_hash AS __object, t_1.label_hash AS __label
   222  	FROM quads AS t_1, (SELECT subject_hash AS ` + tagNode + ` FROM quads WHERE predicate_hash = $1 LIMIT 10) AS t_2
   223  	WHERE t_1.subject_hash = $2 AND t_1.predicate_hash = t_2.` + tagNode,
   224  		args: sVals("p", "s"),
   225  	},
   226  	{
   227  		skip: true, // TODO
   228  		name: "quads with subquery (inner tags + limit)",
   229  		s: shape.Quads{
   230  			{Dir: quad.Subject, Values: shape.Fixed{sVal("s")}},
   231  			{
   232  				Dir: quad.Predicate,
   233  				Values: shape.Save{
   234  					Tags: []string{"pred"},
   235  					From: shape.Page{
   236  						Limit: 10,
   237  						From: shape.QuadsAction{
   238  							Result: quad.Subject,
   239  							Save: map[quad.Direction][]string{
   240  								quad.Object: {"ob"},
   241  							},
   242  							Filter: map[quad.Direction]graph.Ref{
   243  								quad.Predicate: sVal("p"),
   244  							},
   245  						},
   246  					},
   247  				},
   248  			},
   249  		},
   250  		qu:   ``,
   251  		args: []Value{},
   252  	},
   253  	{
   254  		name: "nodes from quads",
   255  		s: shape.NodesFrom{
   256  			Dir: quad.Object,
   257  			Quads: shape.Quads{
   258  				{Dir: quad.Subject, Values: shape.Fixed{sVal("s")}},
   259  				{
   260  					Dir: quad.Predicate,
   261  					Values: shape.QuadsAction{
   262  						Result: quad.Subject,
   263  						Save: map[quad.Direction][]string{
   264  							quad.Object: {"ob"},
   265  						},
   266  						Filter: map[quad.Direction]graph.Ref{
   267  							quad.Predicate: sVal("p"),
   268  						},
   269  					},
   270  				},
   271  			},
   272  		},
   273  		qu: `SELECT t_1.object_hash AS ` + tagNode + `, t_2.object_hash AS ob
   274  	FROM quads AS t_1, quads AS t_2
   275  	WHERE t_1.subject_hash = $1 AND t_2.predicate_hash = $2 AND t_1.predicate_hash = t_2.subject_hash`,
   276  		args: sVals("s", "p"),
   277  	},
   278  	{
   279  		name: "intersect selects",
   280  		s: shape.Intersect{
   281  			shape.Save{
   282  				Tags: []string{"sub"},
   283  				From: shape.QuadsAction{
   284  					Result: quad.Subject,
   285  					Save: map[quad.Direction][]string{
   286  						quad.Object: {"o1"},
   287  						quad.Label:  {"l 1"},
   288  					},
   289  					Filter: map[quad.Direction]graph.Ref{
   290  						quad.Predicate: sVal("p1"),
   291  					},
   292  				},
   293  			},
   294  			shape.NodesFrom{
   295  				Dir: quad.Object,
   296  				Quads: shape.Quads{
   297  					{Dir: quad.Subject, Values: shape.Fixed{sVal("s")}},
   298  					{
   299  						Dir: quad.Predicate,
   300  						Values: shape.QuadsAction{
   301  							Result: quad.Subject,
   302  							Save: map[quad.Direction][]string{
   303  								quad.Object: {"ob"},
   304  							},
   305  							Filter: map[quad.Direction]graph.Ref{
   306  								quad.Predicate: sVal("p2"),
   307  							},
   308  						},
   309  					},
   310  				},
   311  			},
   312  		},
   313  		qu: `SELECT t_3.subject_hash AS sub, t_3.subject_hash AS __node, t_3.object_hash AS o1, t_3.label_hash AS "l 1", t_2.object_hash AS ob
   314  	FROM quads AS t_3, quads AS t_1, quads AS t_2
   315  	WHERE t_3.predicate_hash = $1 AND t_1.subject_hash = $2 AND t_2.predicate_hash = $3 AND t_1.predicate_hash = t_2.subject_hash AND t_3.subject_hash = t_1.object_hash`,
   316  		args: sVals("p1", "s", "p2"),
   317  	},
   318  	{
   319  		name: "deep shape",
   320  		s: shape.NodesFrom{
   321  			Dir: quad.Object,
   322  			Quads: shape.Quads{
   323  				shape.QuadFilter{Dir: quad.Predicate, Values: shape.Fixed{sVal("s")}},
   324  				shape.QuadFilter{
   325  					Dir: quad.Subject,
   326  					Values: shape.NodesFrom{
   327  						Dir: quad.Subject,
   328  						Quads: shape.Quads{
   329  							shape.QuadFilter{Dir: quad.Predicate, Values: shape.Fixed{sVal("s")}},
   330  							shape.QuadFilter{
   331  								Dir: quad.Object,
   332  								Values: shape.NodesFrom{
   333  									Dir: quad.Subject,
   334  									Quads: shape.Quads{
   335  										shape.QuadFilter{Dir: quad.Predicate, Values: shape.Fixed{sVal("a")}},
   336  										shape.QuadFilter{
   337  											Dir: quad.Object,
   338  											Values: shape.QuadsAction{
   339  												Result: quad.Subject,
   340  												Filter: map[quad.Direction]graph.Ref{
   341  													quad.Predicate: sVal("n"),
   342  													quad.Object:    sVal("k"),
   343  												},
   344  											},
   345  										},
   346  									},
   347  								},
   348  							},
   349  						},
   350  					},
   351  				},
   352  			},
   353  		},
   354  		qu:   `SELECT t_5.object_hash AS __node FROM quads AS t_5, (SELECT t_3.subject_hash AS __node FROM quads AS t_3, (SELECT t_1.subject_hash AS __node FROM quads AS t_1, (SELECT subject_hash AS __node FROM quads WHERE predicate_hash = $1 AND object_hash = $2) AS t_2 WHERE t_1.predicate_hash = $3 AND t_1.object_hash = t_2.__node) AS t_4 WHERE t_3.predicate_hash = $4 AND t_3.object_hash = t_4.__node) AS t_6 WHERE t_5.predicate_hash = $5 AND t_5.subject_hash = t_6.__node`,
   355  		args: sVals("n", "k", "a", "s", "s"),
   356  	},
   357  }
   358  
   359  func TestSQLShapes(t *testing.T) {
   360  	dialect := DefaultDialect
   361  	dialect.Placeholder = func(i int) string {
   362  		return fmt.Sprintf("$%d", i)
   363  	}
   364  	for _, c := range shapeCases {
   365  		t.Run(c.name, func(t *testing.T) {
   366  			opt := NewOptimizer()
   367  			s, ok := c.s.Optimize(opt)
   368  			if c.skip {
   369  				t.Skipf("%#v", s)
   370  			}
   371  			require.True(t, ok, "%#v", s)
   372  			sq, ok := s.(Shape)
   373  			require.True(t, ok, "%#v", s)
   374  			b := NewBuilder(dialect)
   375  			require.Equal(t, c.qu, sq.SQL(b), "%#v", sq)
   376  			require.Equal(t, c.args, sq.Args())
   377  		})
   378  	}
   379  }