@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.
- package/CHANGELOG.md +16 -0
- package/dist/Emailer/Sendgrid.d.ts +1 -1
- package/dist/Model/Repository/internal/internal.d.ts +1 -1
- package/dist/Model/Repository/internal/internal.d.ts.map +1 -1
- package/dist/Model/Repository/internal/internal.js +22 -12
- package/dist/Model/query/dsl.d.ts +71 -2
- package/dist/Model/query/dsl.d.ts.map +1 -1
- package/dist/Model/query/dsl.js +47 -1
- package/dist/Model/query/new-kid-interpreter.d.ts +38 -2
- package/dist/Model/query/new-kid-interpreter.d.ts.map +1 -1
- package/dist/Model/query/new-kid-interpreter.js +49 -2
- package/dist/Store/Cosmos/query.d.ts +8 -2
- package/dist/Store/Cosmos/query.d.ts.map +1 -1
- package/dist/Store/Cosmos/query.js +67 -13
- package/dist/Store/Memory.d.ts +1 -1
- package/dist/Store/Memory.d.ts.map +1 -1
- package/dist/Store/Memory.js +72 -5
- package/dist/Store/SQL/query.d.ts +8 -2
- package/dist/Store/SQL/query.d.ts.map +1 -1
- package/dist/Store/SQL/query.js +47 -3
- package/dist/Store/service.d.ts +8 -2
- package/dist/Store/service.d.ts.map +1 -1
- package/dist/Store/service.js +1 -1
- package/package.json +2 -2
- package/src/Model/Repository/internal/internal.ts +19 -1
- package/src/Model/query/dsl.ts +95 -1
- package/src/Model/query/new-kid-interpreter.ts +64 -2
- package/src/Store/Cosmos/query.ts +78 -17
- package/src/Store/Memory.ts +76 -5
- package/src/Store/SQL/query.ts +61 -3
- package/src/Store/service.ts +7 -1
- package/test/cosmos-query.test.ts +64 -0
- package/test/query.test.ts +82 -0
- package/test/sql-store.test.ts +119 -0
- 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
|
+
})
|
package/test/query.test.ts
CHANGED
|
@@ -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
|
+
})
|
package/test/sql-store.test.ts
CHANGED
|
@@ -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":""}
|