Skip to content

Commit 634c266

Browse files
authored
Merge pull request #1408 from digitallyinduced/s0kil/fix-issue-1407
DataSync Support distinctOn
2 parents 0736b4f + b9b0a73 commit 634c266

File tree

6 files changed

+79
-27
lines changed

6 files changed

+79
-27
lines changed

Guide/realtime-spas.markdown

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -689,7 +689,7 @@ const todos = await query('todos')
689689
.where('id', 'd94173ec-1d91-421e-8fdc-20a3161b7802')
690690
.or(where('id', '173ecd94-911d-1e42-dc8f-1b780320a316'))
691691
.fetch()
692-
692+
693693
// SQL:
694694
// SELECT * FROM todos
695695
// WHERE id = 'd94173ec-1d91-421e-8fdc-20a3161b7802'
@@ -711,7 +711,7 @@ const todos = await query('todos')
711711
)
712712
)
713713
.fetch()
714-
714+
715715
// SQL:
716716
// SELECT * FROM todos
717717
// WHERE id = 'd94173ec-1d91-421e-8fdc-20a3161b7802'
@@ -754,7 +754,7 @@ const todos = await query('todos')
754754
)
755755
)
756756
.fetch()
757-
757+
758758
// SQL:
759759
// SELECT * FROM todos
760760
// WHERE user_id = '173ecd94-911d-1e42-dc8f-1b780320a316'
@@ -791,6 +791,17 @@ const oldestTodos = await query('todos')
791791
.fetchOne();
792792
````
793793
794+
795+
#### Unique/Distinct
796+
797+
Use `distinctOn`, to get a unique result:
798+
799+
```javascript
800+
const userTodos = await query('todos')
801+
.distinctOn('userId')
802+
.fetch();
803+
````
804+
794805
### Create Record
795806
796807
To insert a record into the database, call `createRecord` with a plain javascript object:

IHP/DataSync/DynamicQuery.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ data DynamicSQLQuery = DynamicSQLQuery
4747
, selectedColumns :: SelectedColumns
4848
, whereCondition :: !(Maybe ConditionExpression)
4949
, orderByClause :: ![OrderByClause]
50+
, distinctOnColumn :: !(Maybe ByteString)
5051
, limit :: !(Maybe Int)
5152
, offset :: !(Maybe Int)
5253
} deriving (Show, Eq)
@@ -178,6 +179,7 @@ instance FromJSON DynamicSQLQuery where
178179
<*> v .: "selectedColumns"
179180
<*> v .: "whereCondition"
180181
<*> v .: "orderByClause"
182+
<*> v .:? "distinctOnColumn" -- distinctOnColumn can be absent in older versions of ihp-datasync.js
181183
<*> v .:? "limit" -- Limit can be absent in older versions of ihp-datasync.js
182184
<*> v .:? "offset" -- Offset can be absent in older versions of ihp-datasync.js
183185

@@ -194,4 +196,3 @@ instance FromJSON OrderByClause where
194196
"OrderByTSRank" -> OrderByTSRank <$> v .: "tsvector" <*> v .: "tsquery"
195197
otherwise -> error ("Invalid tag: " <> otherwise)
196198
tagged <|> oldFormat
197-

IHP/DataSync/DynamicQueryCompiler.hs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,21 @@ import qualified Data.List as List
1919
compileQuery :: DynamicSQLQuery -> (PG.Query, [PG.Action])
2020
compileQuery DynamicSQLQuery { .. } = (sql, args)
2121
where
22-
sql = "SELECT ? FROM ?" <> whereSql <> orderBySql <> limitSql <> offsetSql
23-
args = catMaybes
24-
[ Just (compileSelectedColumns selectedColumns)
25-
, Just (PG.toField (PG.Identifier table))
26-
]
22+
sql = "SELECT" <> distinctOnSql <> "? FROM ?" <> whereSql <> orderBySql <> limitSql <> offsetSql
23+
args = distinctOnArgs
24+
<> catMaybes
25+
[ Just (compileSelectedColumns selectedColumns)
26+
, Just (PG.toField (PG.Identifier table))
27+
]
2728
<> whereArgs
2829
<> orderByArgs
2930
<> limitArgs
3031
<> offsetArgs
3132

33+
(distinctOnSql, distinctOnArgs) = case distinctOnColumn of
34+
Just column -> (" DISTINCT ON (?) ", [PG.toField $ PG.Identifier (fieldNameToColumnName $ cs column)])
35+
Nothing -> (" ", [])
36+
3237
(orderBySql, orderByArgs) = case orderByClause of
3338
[] -> ("", [])
3439
orderByClauses ->
@@ -56,7 +61,7 @@ compileQuery DynamicSQLQuery { .. } = (sql, args)
5661
(limitSql, limitArgs) = case limit of
5762
Just limit -> (" LIMIT ?", [PG.toField limit])
5863
Nothing -> ("", [])
59-
64+
6065
(offsetSql, offsetArgs) = case offset of
6166
Just offset -> (" OFFSET ?", [PG.toField offset])
6267
Nothing -> ("", [])
@@ -115,4 +120,4 @@ compileOperator OpAnd = "AND"
115120
compileOperator OpOr = "OR"
116121
compileOperator OpIs = "IS"
117122
compileOperator OpIsNot = "IS NOT"
118-
compileOperator OpTSMatch = "@@"
123+
compileOperator OpTSMatch = "@@"

IHP/DataSync/REST/Controller.hs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ instance (
5555
|> map aesonValueToPostgresValue
5656

5757
let params = (PG.Identifier table, PG.In (map PG.Identifier columns), PG.In values)
58-
58+
5959
result :: Either EnhancedSqlError [[Field]] <- Exception.try do
6060
sqlQueryWithRLS query params
6161

@@ -87,14 +87,14 @@ instance (
8787
|> HashMap.elems
8888
|> map aesonValueToPostgresValue
8989
)
90-
90+
9191

9292
let params = (PG.Identifier table, PG.In (map PG.Identifier columns), PG.Values [] values)
9393

9494
result :: [[Field]] <- sqlQueryWithRLS query params
9595
renderJson result
9696

97-
97+
9898

9999
action UpdateRecordAction { table, id } = do
100100
ensureRLSEnabled table
@@ -170,6 +170,7 @@ buildDynamicQueryFromRequest table = DynamicSQLQuery
170170
, selectedColumns = paramOrDefault SelectAll "fields"
171171
, whereCondition = Nothing
172172
, orderByClause = paramList "orderBy"
173+
, distinctOnColumn = paramOrNothing "distinctOnColumn"
173174
, limit = paramOrNothing "limit"
174175
, offset = paramOrNothing "offset"
175176
}
@@ -220,18 +221,18 @@ aesonValueToPostgresValue (Number value) = case Scientific.floatingOrInteger val
220221
Left (floating :: Double) -> PG.toField floating
221222
Right (integer :: Integer) -> PG.toField integer
222223
aesonValueToPostgresValue Data.Aeson.Null = PG.toField PG.Null
223-
aesonValueToPostgresValue object@(Object values) =
224+
aesonValueToPostgresValue object@(Object values) =
224225
let
225226
tryDecodeAsPoint :: Maybe Point
226227
tryDecodeAsPoint = do
227228
xValue <- HashMap.lookup "x" values
228229
yValue <- HashMap.lookup "y" values
229230
x <- case xValue of
230231
Number number -> pure (Scientific.toRealFloat number)
231-
otherwise -> Nothing
232+
otherwise -> Nothing
232233
y <- case yValue of
233234
Number number -> pure (Scientific.toRealFloat number)
234-
otherwise -> Nothing
235+
otherwise -> Nothing
235236
pure Point { x, y }
236237
in
237238
-- This is really hacky and is mostly duck typing. We should refactor this in the future to

Test/DataSync/DynamicQueryCompiler.hs

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ tests = do
1919
, selectedColumns = SelectAll
2020
, whereCondition = Nothing
2121
, orderByClause = []
22+
, distinctOnColumn = Nothing
2223
, limit = Nothing
2324
, offset = Nothing
2425
}
@@ -27,12 +28,13 @@ tests = do
2728
( "SELECT ? FROM ?"
2829
, [PG.Plain "*", PG.EscapeIdentifier "posts"]
2930
)
30-
31+
3132
it "compile a select query with order by" do
3233
let query = DynamicSQLQuery
3334
{ table = "posts"
3435
, selectedColumns = SelectAll
3536
, whereCondition = Nothing
37+
, distinctOnColumn = Nothing
3638
, orderByClause = [OrderByClause { orderByColumn = "title", orderByDirection = Desc }]
3739
, limit = Nothing
3840
, offset = Nothing
@@ -42,7 +44,7 @@ tests = do
4244
( "SELECT ? FROM ? ORDER BY ? ?"
4345
, [PG.Plain "*", PG.EscapeIdentifier "posts", PG.EscapeIdentifier "title", PG.Plain "DESC"]
4446
)
45-
47+
4648
it "compile a select query with multiple order bys" do
4749
let query = DynamicSQLQuery
4850
{ table = "posts"
@@ -52,6 +54,7 @@ tests = do
5254
OrderByClause { orderByColumn = "createdAt", orderByDirection = Desc },
5355
OrderByClause { orderByColumn = "title", orderByDirection = Asc }
5456
]
57+
, distinctOnColumn = Nothing
5558
, limit = Nothing
5659
, offset = Nothing
5760
}
@@ -60,13 +63,14 @@ tests = do
6063
( "SELECT ? FROM ? ORDER BY ? ?, ? ?"
6164
, [PG.Plain "*", PG.EscapeIdentifier "posts", PG.EscapeIdentifier "created_at", PG.Plain "DESC", PG.EscapeIdentifier "title", PG.Plain ""]
6265
)
63-
66+
6467
it "compile a basic select query with a where condition" do
6568
let query = DynamicSQLQuery
6669
{ table = "posts"
6770
, selectedColumns = SelectAll
6871
, whereCondition = Just $ InfixOperatorExpression (ColumnExpression "userId") OpEqual (LiteralExpression (TextValue "b8553ce9-6a42-4a68-b5fc-259be3e2acdc"))
6972
, orderByClause = []
73+
, distinctOnColumn = Nothing
7074
, limit = Nothing
7175
, offset = Nothing
7276
}
@@ -82,6 +86,7 @@ tests = do
8286
, selectedColumns = SelectAll
8387
, whereCondition = Just $ InfixOperatorExpression (ColumnExpression "userId") OpEqual (LiteralExpression (TextValue "b8553ce9-6a42-4a68-b5fc-259be3e2acdc"))
8488
, orderByClause = [ OrderByClause { orderByColumn = "createdAt", orderByDirection = Desc } ]
89+
, distinctOnColumn = Nothing
8590
, limit = Nothing
8691
, offset = Nothing
8792
}
@@ -96,6 +101,7 @@ tests = do
96101
{ table = "posts"
97102
, selectedColumns = SelectAll
98103
, whereCondition = Just $ InfixOperatorExpression (ColumnExpression "userId") OpEqual (LiteralExpression (TextValue "b8553ce9-6a42-4a68-b5fc-259be3e2acdc"))
104+
, distinctOnColumn = Nothing
99105
, orderByClause = []
100106
, limit = Just 50
101107
, offset = Nothing
@@ -112,6 +118,7 @@ tests = do
112118
, selectedColumns = SelectAll
113119
, whereCondition = Just $ InfixOperatorExpression (ColumnExpression "userId") OpEqual (LiteralExpression (TextValue "b8553ce9-6a42-4a68-b5fc-259be3e2acdc"))
114120
, orderByClause = []
121+
, distinctOnColumn = Nothing
115122
, limit = Nothing
116123
, offset = Just 50
117124
}
@@ -127,25 +134,27 @@ tests = do
127134
, selectedColumns = SelectAll
128135
, whereCondition = Just $ InfixOperatorExpression (ColumnExpression "userId") OpEqual (LiteralExpression (TextValue "b8553ce9-6a42-4a68-b5fc-259be3e2acdc"))
129136
, orderByClause = []
137+
, distinctOnColumn = Nothing
130138
, limit = Just 25
131139
, offset = Just 50
132140
}
133-
141+
134142
compileQuery query `shouldBe`
135143
( "SELECT ? FROM ? WHERE (?) = (?) LIMIT ? OFFSET ?"
136144
, [PG.Plain "*", PG.EscapeIdentifier "posts", PG.EscapeIdentifier "user_id", PG.Escape "b8553ce9-6a42-4a68-b5fc-259be3e2acdc", PG.Plain "25", PG.Plain "50"]
137145
)
138-
146+
139147
it "compile 'field = NULL' conditions to 'field IS NULL'" do
140148
let query = DynamicSQLQuery
141149
{ table = "posts"
142150
, selectedColumns = SelectAll
143151
, whereCondition = Just $ InfixOperatorExpression (ColumnExpression "userId") OpEqual NullExpression
144152
, orderByClause = []
153+
, distinctOnColumn = Nothing
145154
, limit = Nothing
146155
, offset = Nothing
147156
}
148-
157+
149158
compileQuery query `shouldBe`
150159
( "SELECT ? FROM ? WHERE (?) IS NULL"
151160
, [PG.Plain "*", PG.EscapeIdentifier "posts", PG.EscapeIdentifier "user_id"]
@@ -157,10 +166,11 @@ tests = do
157166
, selectedColumns = SelectAll
158167
, whereCondition = Just $ InfixOperatorExpression (ColumnExpression "userId") OpNotEqual NullExpression
159168
, orderByClause = []
169+
, distinctOnColumn = Nothing
160170
, limit = Nothing
161171
, offset = Nothing
162172
}
163-
173+
164174
compileQuery query `shouldBe`
165175
( "SELECT ? FROM ? WHERE (?) IS NOT NULL"
166176
, [PG.Plain "*", PG.EscapeIdentifier "posts", PG.EscapeIdentifier "user_id"]
@@ -172,11 +182,28 @@ tests = do
172182
, selectedColumns = SelectAll
173183
, whereCondition = Just $ InfixOperatorExpression (ColumnExpression "ts") OpTSMatch (CallExpression { functionCall = ToTSQuery { text = "test" }})
174184
, orderByClause = [ OrderByTSRank { tsvector = "ts", tsquery = "test" } ]
185+
, distinctOnColumn = Nothing
175186
, limit = Nothing
176187
, offset = Nothing
177188
}
178-
189+
179190
compileQuery query `shouldBe`
180191
( "SELECT ? FROM ? WHERE (?) @@ (to_tsquery('english', ?)) ORDER BY ts_rank(?, to_tsquery('english', ?))"
181192
, [PG.Plain "*", PG.EscapeIdentifier "products", PG.EscapeIdentifier "ts", PG.Escape "test", PG.EscapeIdentifier "ts", PG.Escape "test"]
182-
)
193+
)
194+
195+
it "compile a basic select query with distinctOn" do
196+
let query = DynamicSQLQuery
197+
{ table = "posts"
198+
, selectedColumns = SelectAll
199+
, whereCondition = Nothing
200+
, orderByClause = []
201+
, distinctOnColumn = Just "groupId"
202+
, limit = Nothing
203+
, offset = Nothing
204+
}
205+
206+
compileQuery query `shouldBe`
207+
( "SELECT DISTINCT ON (?) ? FROM ?"
208+
, [PG.EscapeIdentifier "group_id", PG.Plain "*", PG.EscapeIdentifier "posts"]
209+
)

lib/IHP/DataSync/ihp-querybuilder.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ class QueryBuilder extends ConditionBuildable {
229229
selectedColumns: { tag: 'SelectAll' },
230230
whereCondition: null,
231231
orderByClause: [],
232+
distinctOnColumn: null,
232233
limit: null,
233234
offset: null
234235
};
@@ -308,6 +309,12 @@ class QueryBuilder extends ConditionBuildable {
308309
return this;
309310
}
310311

312+
distinctOn(column) {
313+
this.query.distinctOnColumn = column
314+
315+
return this
316+
}
317+
311318
limit(limit) {
312319
if (limit !== null && !Number.isInteger(limit)) {
313320
throw new Error('limit needs to be an integer, or null if no limit should be used');
@@ -339,7 +346,7 @@ class QueryBuilder extends ConditionBuildable {
339346
const result = await this.limit(1).fetch();
340347
return result.length > 0 ? result[0] : null;
341348
}
342-
349+
343350
subscribe(callback) {
344351
const dataSubscription = new DataSubscription(this.query);
345352
dataSubscription.createOnServer();

0 commit comments

Comments
 (0)