@effect-app/infra 4.0.0-beta.229 → 4.0.0-beta.230

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/Emailer/Sendgrid.d.ts +1 -1
  3. package/dist/Model/Repository/internal/internal.d.ts +1 -1
  4. package/dist/Model/Repository/internal/internal.d.ts.map +1 -1
  5. package/dist/Model/Repository/internal/internal.js +22 -12
  6. package/dist/Model/query/dsl.d.ts +71 -2
  7. package/dist/Model/query/dsl.d.ts.map +1 -1
  8. package/dist/Model/query/dsl.js +47 -1
  9. package/dist/Model/query/new-kid-interpreter.d.ts +38 -2
  10. package/dist/Model/query/new-kid-interpreter.d.ts.map +1 -1
  11. package/dist/Model/query/new-kid-interpreter.js +49 -2
  12. package/dist/Store/Cosmos/query.d.ts +8 -2
  13. package/dist/Store/Cosmos/query.d.ts.map +1 -1
  14. package/dist/Store/Cosmos/query.js +67 -13
  15. package/dist/Store/Memory.d.ts +1 -1
  16. package/dist/Store/Memory.d.ts.map +1 -1
  17. package/dist/Store/Memory.js +72 -5
  18. package/dist/Store/SQL/query.d.ts +8 -2
  19. package/dist/Store/SQL/query.d.ts.map +1 -1
  20. package/dist/Store/SQL/query.js +47 -3
  21. package/dist/Store/service.d.ts +8 -2
  22. package/dist/Store/service.d.ts.map +1 -1
  23. package/dist/Store/service.js +1 -1
  24. package/package.json +2 -2
  25. package/src/Model/Repository/internal/internal.ts +19 -1
  26. package/src/Model/query/dsl.ts +95 -1
  27. package/src/Model/query/new-kid-interpreter.ts +64 -2
  28. package/src/Store/Cosmos/query.ts +78 -17
  29. package/src/Store/Memory.ts +76 -5
  30. package/src/Store/SQL/query.ts +61 -3
  31. package/src/Store/service.ts +7 -1
  32. package/test/cosmos-query.test.ts +64 -0
  33. package/test/query.test.ts +82 -0
  34. package/test/sql-store.test.ts +119 -0
  35. package/test/dist/date-query.test.d.ts.map +0 -1
@@ -93,3 +93,67 @@ describe("cosmos query projection: relation-every parameter binding", () => {
93
93
  expect(refs).toEqual(["@v0", "@v1", "@v2", "@v3"])
94
94
  })
95
95
  })
96
+
97
+ describe("cosmos query: aggregate (GROUP BY + agg functions)", () => {
98
+ it("generates GROUP BY and SUM(IIF(...)) for agg-count-when", () => {
99
+ const result = buildWhereCosmosQuery3(
100
+ "id",
101
+ [],
102
+ "Orders",
103
+ {},
104
+ [
105
+ { key: "city", path: "address.city" },
106
+ {
107
+ key: "activeCount",
108
+ aggregate: {
109
+ _tag: "agg-count-when",
110
+ filter: [{ t: "where", path: "status", op: "eq", value: "active" }]
111
+ }
112
+ },
113
+ { key: "total", aggregate: { _tag: "agg-count" } }
114
+ ] as any
115
+ )
116
+
117
+ expect(result.query).toContain("GROUP BY")
118
+ expect(result.query).toContain("SUM(IIF(")
119
+ expect(result.query).toContain("COUNT(1) AS total")
120
+ expect(result.query).toContain("AS city")
121
+ expect(result.parameters[0]!.value).toBe("active")
122
+ })
123
+
124
+ it("generates agg-sum / agg-min / agg-max expressions", () => {
125
+ const result = buildWhereCosmosQuery3(
126
+ "id",
127
+ [],
128
+ "Orders",
129
+ {},
130
+ [
131
+ { key: "dept", path: "dept" },
132
+ { key: "totalSalary", aggregate: { _tag: "agg-sum", field: "salary" } },
133
+ { key: "minSalary", aggregate: { _tag: "agg-min", field: "salary" } },
134
+ { key: "maxSalary", aggregate: { _tag: "agg-max", field: "salary" } }
135
+ ] as any
136
+ )
137
+
138
+ expect(result.query).toContain(`SUM(f["salary"]) AS totalSalary`)
139
+ expect(result.query).toContain(`MIN(f["salary"]) AS minSalary`)
140
+ expect(result.query).toContain(`MAX(f["salary"]) AS maxSalary`)
141
+ expect(result.query).toMatch(/GROUP BY f(?:\.dept|\["dept"\])/)
142
+ })
143
+
144
+ it("generates correct GROUP BY for nested path field", () => {
145
+ const result = buildWhereCosmosQuery3(
146
+ "id",
147
+ [],
148
+ "Orders",
149
+ {},
150
+ [
151
+ { key: "city", path: "address.city" },
152
+ { key: "count", aggregate: { _tag: "agg-count" } }
153
+ ] as any
154
+ )
155
+
156
+ // Must GROUP BY the nested path in Cosmos access notation
157
+ expect(result.query).toMatch(/GROUP BY f(?:\.address\.city|\["address"\]\["city"\])/)
158
+ })
159
+ })
@@ -1877,3 +1877,85 @@ it("codeFilter: order + skip + limit applied after filter", () => {
1877
1877
  )
1878
1878
  expect(runCF(q)).toEqual(["2", "1"])
1879
1879
  })
1880
+
1881
+ // ─────────────────────────────────────────────────────────────────────────────
1882
+ // memFilter: aggregate (GROUP BY) support
1883
+ // ─────────────────────────────────────────────────────────────────────────────
1884
+
1885
+ it("memFilter: agg-count-when groups rows and counts conditionally", () => {
1886
+ type Row = { city: string; status: string }
1887
+ const rows: Row[] = [
1888
+ { city: "NYC", status: "active" },
1889
+ { city: "NYC", status: "inactive" },
1890
+ { city: "NYC", status: "active" },
1891
+ { city: "LA", status: "active" }
1892
+ ]
1893
+ const result = memFilter<Row>({
1894
+ t: {} as Row,
1895
+ select: [
1896
+ { key: "city", path: "city" },
1897
+ {
1898
+ key: "activeCount",
1899
+ aggregate: {
1900
+ _tag: "agg-count-when",
1901
+ filter: [{ t: "where", path: "status", op: "eq", value: "active" }]
1902
+ }
1903
+ },
1904
+ { key: "total", aggregate: { _tag: "agg-count" } }
1905
+ ] as any
1906
+ })(rows as any) as any[]
1907
+
1908
+ expect(result.length).toBe(2)
1909
+ const nyc = result.find((r: any) => r.city === "NYC")!
1910
+ expect(nyc.activeCount).toBe(2)
1911
+ expect(nyc.total).toBe(3)
1912
+ const la = result.find((r: any) => r.city === "LA")!
1913
+ expect(la.activeCount).toBe(1)
1914
+ expect(la.total).toBe(1)
1915
+ })
1916
+
1917
+ it("memFilter: agg-sum / agg-min / agg-max aggregate numerics", () => {
1918
+ type Row = { dept: string; salary: number }
1919
+ const rows: Row[] = [
1920
+ { dept: "eng", salary: 100 },
1921
+ { dept: "eng", salary: 200 },
1922
+ { dept: "hr", salary: 50 }
1923
+ ]
1924
+ const result = memFilter<Row>({
1925
+ t: {} as Row,
1926
+ select: [
1927
+ { key: "dept", path: "dept" },
1928
+ { key: "total", aggregate: { _tag: "agg-sum", field: "salary" } },
1929
+ { key: "min", aggregate: { _tag: "agg-min", field: "salary" } },
1930
+ { key: "max", aggregate: { _tag: "agg-max", field: "salary" } }
1931
+ ] as any
1932
+ })(rows as any) as any[]
1933
+
1934
+ expect(result.length).toBe(2)
1935
+ const eng = result.find((r: any) => r.dept === "eng")!
1936
+ expect(eng.total).toBe(300)
1937
+ expect(eng.min).toBe(100)
1938
+ expect(eng.max).toBe(200)
1939
+ const hr = result.find((r: any) => r.dept === "hr")!
1940
+ expect(hr.total).toBe(50)
1941
+ })
1942
+
1943
+ it("memFilter: aggregate with nested path grouping", () => {
1944
+ type Row = { address: { city: string }; value: number }
1945
+ const rows: Row[] = [
1946
+ { address: { city: "NYC" }, value: 10 },
1947
+ { address: { city: "NYC" }, value: 20 },
1948
+ { address: { city: "LA" }, value: 5 }
1949
+ ]
1950
+ const result = memFilter<Row>({
1951
+ t: {} as Row,
1952
+ select: [
1953
+ { key: "city", path: "address.city" },
1954
+ { key: "count", aggregate: { _tag: "agg-count" } }
1955
+ ] as any
1956
+ })(rows as any) as any[]
1957
+
1958
+ expect(result.length).toBe(2)
1959
+ expect(result.find((r: any) => r.city === "NYC")!.count).toBe(2)
1960
+ expect(result.find((r: any) => r.city === "LA")!.count).toBe(1)
1961
+ })
@@ -1157,6 +1157,125 @@ describe("SQL Store (SQLite integration)", () => {
1157
1157
  const names = results.map((r) => (JSON.parse((r as any).data) as any).name).sort()
1158
1158
  expect(names).toEqual(["Alice", "Bob"])
1159
1159
  }))
1160
+
1161
+ it("aggregate: agg-count-when with GROUP BY on aliased path field (SQLite)", () =>
1162
+ withDb((db) => {
1163
+ db.exec(`CREATE TABLE "test_agg" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
1164
+ const rows = [
1165
+ { city: "NYC", state: "active" },
1166
+ { city: "NYC", state: "inactive" },
1167
+ { city: "NYC", state: "active" },
1168
+ { city: "LA", state: "active" }
1169
+ ]
1170
+ const insert = db.prepare(`INSERT INTO "test_agg" (id, _etag, data) VALUES (?, ?, ?)`)
1171
+ rows.forEach((r, i) => insert.run(String(i + 1), `e${i + 1}`, JSON.stringify(r)))
1172
+
1173
+ const q = buildWhereSQLQuery(
1174
+ sqliteDialect,
1175
+ "id",
1176
+ [],
1177
+ "test_agg",
1178
+ {},
1179
+ [
1180
+ { key: "city", path: "city" },
1181
+ {
1182
+ key: "activeCount",
1183
+ aggregate: {
1184
+ _tag: "agg-count-when",
1185
+ filter: [{ t: "where", path: "state", op: "eq", value: "active" }]
1186
+ }
1187
+ },
1188
+ { key: "total", aggregate: { _tag: "agg-count" } }
1189
+ ] as any,
1190
+ [{ key: "city", direction: "ASC" }] as any
1191
+ )
1192
+
1193
+ expect(q.sql).toContain("GROUP BY")
1194
+ expect(q.sql).toContain("COUNT(CASE WHEN")
1195
+
1196
+ const result = query(db, q.sql, q.params) as Array<Record<string, unknown>>
1197
+ expect(result.length).toBe(2)
1198
+
1199
+ const la = result.find((r) => r["city"] === "LA")!
1200
+ expect(la["activeCount"]).toBe(1)
1201
+ expect(la["total"]).toBe(1)
1202
+
1203
+ const nyc = result.find((r) => r["city"] === "NYC")!
1204
+ expect(nyc["activeCount"]).toBe(2)
1205
+ expect(nyc["total"]).toBe(3)
1206
+ }))
1207
+
1208
+ it("aggregate: agg-sum and agg-min / agg-max (SQLite)", () =>
1209
+ withDb((db) => {
1210
+ db.exec(`CREATE TABLE "test_agg2" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
1211
+ const rows = [
1212
+ { dept: "eng", salary: 100 },
1213
+ { dept: "eng", salary: 200 },
1214
+ { dept: "hr", salary: 50 }
1215
+ ]
1216
+ const insert = db.prepare(`INSERT INTO "test_agg2" (id, _etag, data) VALUES (?, ?, ?)`)
1217
+ rows.forEach((r, i) => insert.run(String(i + 1), `e${i + 1}`, JSON.stringify(r)))
1218
+
1219
+ const q = buildWhereSQLQuery(
1220
+ sqliteDialect,
1221
+ "id",
1222
+ [],
1223
+ "test_agg2",
1224
+ {},
1225
+ [
1226
+ { key: "dept", path: "dept" },
1227
+ { key: "totalSalary", aggregate: { _tag: "agg-sum", field: "salary" } },
1228
+ { key: "minSalary", aggregate: { _tag: "agg-min", field: "salary" } },
1229
+ { key: "maxSalary", aggregate: { _tag: "agg-max", field: "salary" } }
1230
+ ] as any,
1231
+ [{ key: "dept", direction: "ASC" }] as any
1232
+ )
1233
+
1234
+ const result = query(db, q.sql, q.params) as Array<Record<string, unknown>>
1235
+ expect(result.length).toBe(2)
1236
+
1237
+ const eng = result.find((r) => r["dept"] === "eng")!
1238
+ expect(eng["totalSalary"]).toBeCloseTo(300)
1239
+ expect(eng["minSalary"]).toBeCloseTo(100)
1240
+ expect(eng["maxSalary"]).toBeCloseTo(200)
1241
+
1242
+ const hr = result.find((r) => r["dept"] === "hr")!
1243
+ expect(hr["totalSalary"]).toBeCloseTo(50)
1244
+ }))
1245
+
1246
+ it("aggregate: nested path field grouping (SQLite)", () =>
1247
+ withDb((db) => {
1248
+ db.exec(`CREATE TABLE "test_agg3" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
1249
+ const rows = [
1250
+ { address: { city: "NYC" }, status: "open" },
1251
+ { address: { city: "NYC" }, status: "closed" },
1252
+ { address: { city: "LA" }, status: "open" }
1253
+ ]
1254
+ const insert = db.prepare(`INSERT INTO "test_agg3" (id, _etag, data) VALUES (?, ?, ?)`)
1255
+ rows.forEach((r, i) => insert.run(String(i + 1), `e${i + 1}`, JSON.stringify(r)))
1256
+
1257
+ const q = buildWhereSQLQuery(
1258
+ sqliteDialect,
1259
+ "id",
1260
+ [],
1261
+ "test_agg3",
1262
+ {},
1263
+ [
1264
+ { key: "city", path: "address.city" },
1265
+ { key: "total", aggregate: { _tag: "agg-count" } }
1266
+ ] as any,
1267
+ [{ key: "city", direction: "ASC" }] as any
1268
+ )
1269
+
1270
+ expect(q.sql).toContain("GROUP BY")
1271
+
1272
+ const result = query(db, q.sql, q.params) as Array<Record<string, unknown>>
1273
+ expect(result.length).toBe(2)
1274
+ const la = result.find((r) => r["city"] === "LA")!
1275
+ expect(la["total"]).toBe(1)
1276
+ const nyc = result.find((r) => r["city"] === "NYC")!
1277
+ expect(nyc["total"]).toBe(2)
1278
+ }))
1160
1279
  })
1161
1280
 
1162
1281
  // --- boolean WHERE clause tests (regression: where("x", true) must work per dialect) ---
@@ -1 +0,0 @@
1
- {"version":3,"file":"date-query.test.d.ts","sourceRoot":"","sources":["../date-query.test.ts"],"names":[],"mappings":""}