@@ -72,11 +72,11 @@ RETURNS text AS $$
72
72
DECLARE
73
73
q text ;
74
74
tab regclass = selector::regclass; -- We need a table, to select from
75
- cols text [];
76
- col text ;
75
+ cols name [];
76
+ col name ;
77
77
sub record;
78
78
pk text = NULL ;
79
- fk record;
79
+ fks record[] ;
80
80
subselects text [];
81
81
predicates text [];
82
82
BEGIN
@@ -92,60 +92,32 @@ BEGIN
92
92
RAISE EXCEPTION ' Unhandled nested selector %(%)' ,
93
93
sub .selector , sub .predicate ;
94
94
END IF;
95
- SELECT col FROM cols(tab) WHERE col = sub .selector INTO col;
95
+ SELECT col FROM cols(tab) WHERE cols . col = sub .selector INTO col;
96
96
CASE
97
97
WHEN FOUND AND sub .body IS NULL THEN -- A simple column reference
98
98
SELECT * FROM graphql .fk (tab)
99
99
WHERE cardinality(cols) = 1 AND cols[1 ] = col
100
- INTO fk; -- TODO: If there's more than one, emit a clear message.
100
+ INTO fks;
101
101
IF FOUND THEN
102
+ IF cardinality(fks) > 1 THEN
103
+ RAISE EXCEPTION ' Multiple candidate foreign keys for %(%)' , tab, col;
104
+ END IF;
102
105
subselects := subselects
103
- || format(' SELECT to_json(%1$I) AS %4$I FROM %1$I'
104
- E' \n '
105
- ' WHERE %1$I.%2$I = %3$I.%4$I' ,
106
- fk .other , fk .refs [1 ], tab, col);
106
+ || format(E' SELECT to_json(%1$I) AS %4$I FROM %1$I\n '
107
+ ' WHERE %1$I.%2$I = %3$I.%4$I' ,
108
+ fks[1 ].other, fks[1 ].refs[1 ], tab, col);
107
109
cols := cols || format(' %I.%I' , ' sub/' || cardinality(subselects), col);
108
110
ELSE
109
111
cols := cols || format(' %I' , col);
110
112
END IF;
111
113
WHEN FOUND AND sub .body IS NOT NULL THEN -- Index into a column
114
+ subselects := subselects || graphql .to_sql (sub.* , tab);
112
115
-- - TODO: Handle nested lookup into JSON, HStore, RECORD
113
116
-- - TODO: If col REFERENCES something, push lookup down to it
117
+ cols := cols || format(' %I.%I' , ' sub/' || cardinality(subselects), col);
114
118
WHEN NOT FOUND THEN -- It might be a reference to another table
115
- SELECT fk.*
116
- FROM graphql .fk (sub .selector ),
117
- LATERAL (SELECT num FROM graphql .cols (tab)
118
- WHERE col = fk .cols [1 ]) AS _
119
- WHERE cardinality(fk .cols ) = 1 AND fk .tab = to_sql .tab
120
- ORDER BY _ .num LIMIT 1 INTO fk;
121
- IF NOT FOUND THEN
122
- RAISE EXCEPTION ' Not able to construct a JOIN for missing column: %' ,
123
- sub .selector ;
124
- END IF;
125
- -- - If:
126
- -- - * Thare are two and only two foreign keys for the other table, and
127
- -- - * All the columns of the table participate in one or the other
128
- -- - foreign key, then
129
- -- - * We can treat the table as a JOIN table and follow the keys.
130
- -- - Otherwise:
131
- -- - * We use the existence of the foreign key to look up the record in
132
- -- - the table that JOINs with us.
133
- -- -
134
- -- - Whenever we are looking at a table that REFERENCES us, we assume it
135
- -- - is a many-to-one relationship; and expect to return an array-valued
136
- -- - result. However, it should be possible to recognize a one-to-one
137
- -- - relationship via the presence of a UNIQUE constraint, in which case
138
- -- - we ought to return a scalar result.
139
- IF FALSE THEN
140
- subquery := subquery
141
- || graphql .to_sql (sub .selector , sub .predicate , sub .body );
142
- -- - Recursion happens in here
143
- ELSE
144
- -- - Recursion happens in here
145
- subquery := subquery
146
- || graphql .to_sql (sub .selector , sub .predicate , sub .body );
147
- END IF;
148
- --
119
+ subselects := subselects || graphql .to_sql (sub.* , tab, sub .selector );
120
+ cols := cols || format(' %I.%I' , ' sub/' || cardinality(subselects), col);
149
121
ELSE
150
122
RAISE EXCEPTION ' Not able to interpret this selector: %' , sub .selector ;
151
123
END CASE;
@@ -186,6 +158,97 @@ BEGIN
186
158
END
187
159
$$ LANGUAGE plpgsql STABLE STRICT;
188
160
161
+ CREATE FUNCTION to_sql (selector text , predicate text , body text , tab regclass)
162
+ RETURNS text AS $$
163
+ DECLARE
164
+ q text ;
165
+ col name;
166
+ typ regtype;
167
+ lookups text [];
168
+ labels text [];
169
+ BEGIN
170
+ SELECT col, typ FROM cols(tab) WHERE cols .col = selector INTO col, typ;
171
+ IF NOT FOUND THEN
172
+ RAISE EXCEPTION ' Did not find column % on table %' , col, tab;
173
+ END IF;
174
+ FOR sub IN SELECT * FROM graphql .parse_many (body) LOOP
175
+ IF sub .predicate IS NOT NULL THEN
176
+ RAISE EXCEPTION ' Not able to handle predicates when following lookups '
177
+ ' into columns (for field % under %.%)' ,
178
+ sub .selector , tab, col;
179
+ END IF;
180
+ CASE typ
181
+ WHEN regtype(' jsonb' ), regtype(' json' ) THEN
182
+ IF sub .body IS NOT NULL THEN -- TODO: Nested JSON lookups
183
+ RAISE EXCEPTION ' Nested JSON lookup is as yet unimplemented' ;
184
+ END IF;
185
+ WHEN regtype(' hstore' ) THEN
186
+ IF sub .body IS NOT NULL THEN
187
+ RAISE EXCEPTION ' No fields below this level (column % is hstore)' ,
188
+ tab, col;
189
+ END IF;
190
+ ELSE
191
+ RAISE EXCEPTION ' Unhandled nested type %s for %s.%s' , typ, tab, col;
192
+ END CASE;
193
+ lookups := lookups || format(' ->%L' , sub .selector );
194
+ labels := labels || format(' %I' , sub .selector );
195
+ END IF;
196
+ q := format(E' SELECT to_json(_) AS %I\n '
197
+ ' FROM (VALUES (%s)) AS _(%s)' ,
198
+ col,
199
+ array_to_string(lookups, ' , ' ),
200
+ array_to_string(labels, ' , ' ));
201
+ END
202
+ $$ LANGUAGE plpgsql STABLE STRICT;
203
+
204
+ CREATE FUNCTION to_sql (selector text ,
205
+ predicate text ,
206
+ body text ,
207
+ tab regclass,
208
+ other regclass)
209
+ RETURNS text AS $$
210
+ DECLARE
211
+ BEGIN
212
+ END
213
+ $$ LANGUAGE plpgsql STABLE STRICT;
214
+
215
+ SELECT fk.*
216
+ FROM graphql .fk (sub .selector ),
217
+ LATERAL (SELECT num FROM graphql .cols (tab)
218
+ WHERE col = fk .cols [1 ]) AS _
219
+ WHERE cardinality(fk .cols ) = 1 AND fk .tab = to_sql .tab
220
+ ORDER BY _ .num LIMIT 1 INTO fk;
221
+ IF NOT FOUND THEN
222
+ RAISE EXCEPTION ' Not able to construct a JOIN for missing column: %' ,
223
+ sub .selector ;
224
+ END IF;
225
+ -- - If:
226
+ -- - * Thare are two and only two foreign keys for the other table, and
227
+ -- - * All the columns of the table participate in one or the other
228
+ -- - foreign key, then
229
+ -- - * We can treat the table as a JOIN table and follow the keys.
230
+ -- - Otherwise:
231
+ -- - * We use the existence of the foreign key to look up the record in
232
+ -- - the table that JOINs with us.
233
+ -- -
234
+ -- - Whenever we are looking at a table that REFERENCES us, we assume it
235
+ -- - is a many-to-one relationship; and expect to return an array-valued
236
+ -- - result. However, it should be possible to recognize a one-to-one
237
+ -- - relationship via the presence of a UNIQUE constraint, in which case
238
+ -- - we ought to return a scalar result.
239
+ IF FALSE THEN
240
+ subselects := subselects || graphql .to_sql (sub.* , tab);
241
+ SELECT json_agg(other.* )
242
+ FROM tab
243
+ JOIN sub .selector ON (pk) = (fk .cols )
244
+ JOIN fks[2 ].tab ON (fk2 .cols ) = (pk(fks[2 ].tab));
245
+ -- - Recursion happens in here
246
+ ELSE
247
+ subselects := subselects || graphql .to_sql (sub.* , tab, fk .others );
248
+ -- - Recursion happens in here
249
+ subquery := subquery
250
+ || graphql .to_sql (sub .selector , sub .predicate , sub .body );
251
+ END IF;
189
252
CREATE FUNCTION parse_many (expr text )
190
253
RETURNS TABLE (selector text , predicate text , body text ) AS $$
191
254
DECLARE
0 commit comments