Skip to content

Commit

Permalink
feat: query filters (#9)
Browse files Browse the repository at this point in the history
* refactor: refactor repository, query bbuilder and build query files

Move things to a more appropriate place, pass queryBuilder around instead of repository, client and
dialect. All of these live on query builder class

* feat: add inQuery and notInQuery filters

These allow passing a semantic layer query to filters which will then be used to generate an SQL
query which will be used with IN or NOT IN expression
  • Loading branch information
retro authored Apr 20, 2024
1 parent 3679eed commit ed450b6
Show file tree
Hide file tree
Showing 18 changed files with 566 additions and 310 deletions.
1 change: 1 addition & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"outdir",
"Paralamas",
"Pista",
"remeda",
"rmrf",
"ryansonshine",
"semantic-layer",
Expand Down
191 changes: 139 additions & 52 deletions src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,37 @@ await describe("semantic layer", async () => {
},
]);
});

await it("can filter by results of another query", async () => {
const query = queryBuilder.buildQuery({
dimensions: ["customers.country"],
order: { "customers.country": "asc" },
filters: [
{
operator: "inQuery",
member: "customers.country",
value: {
dimensions: ["customers.country"],
filters: [
{
operator: "equals",
member: "customers.country",
value: ["Argentina"],
},
],
},
},
],
limit: 10,
});

const result = await client.query<InferSqlQueryResultType<typeof query>>(
query.sql,
query.bindings,
);

assert.deepEqual(result.rows, [{ customers___country: "Argentina" }]);
});
});

await describe("models from sql queries", async () => {
Expand Down Expand Up @@ -1242,6 +1273,26 @@ await describe("semantic layer", async () => {
required: ["operator", "member", "value"],
additionalProperties: false,
},
{
type: "object",
properties: {
operator: { type: "string", const: "inQuery" },
member: { type: "string" },
value: { $ref: "#" },
},
required: ["operator", "member", "value"],
additionalProperties: false,
},
{
type: "object",
properties: {
operator: { type: "string", const: "notInQuery" },
member: { type: "string" },
value: { $ref: "#" },
},
required: ["operator", "member", "value"],
additionalProperties: false,
},
],
},
},
Expand Down Expand Up @@ -1889,62 +1940,62 @@ await describe("semantic layer", async () => {
});

describe("repository with context", async () => {
await it("propagates context to all sql functions", async () => {
type QueryContext = {
customerId: number;
};
type QueryContext = {
customerId: number;
};

const customersModel = semanticLayer
.model<QueryContext>()
.withName("customers")
.fromSqlQuery(
({ sql, identifier, getContext }) =>
sql`select * from ${identifier("Customer")} where ${identifier(
"CustomerId",
)} = ${getContext().customerId}`,
)
.withDimension("customer_id", {
type: "number",
primaryKey: true,
sql: ({ model, sql, getContext }) =>
sql`${model.column("CustomerId")} || cast(${
getContext().customerId
} as text)`,
})
.withDimension("first_name", {
type: "string",
sql: ({ model }) => model.column("FirstName"),
});
const customersModel = semanticLayer
.model<QueryContext>()
.withName("customers")
.fromSqlQuery(
({ sql, identifier, getContext }) =>
sql`select * from ${identifier("Customer")} where ${identifier(
"CustomerId",
)} = ${getContext().customerId}`,
)
.withDimension("customer_id", {
type: "number",
primaryKey: true,
sql: ({ model, sql, getContext }) =>
sql`${model.column("CustomerId")} || cast(${
getContext().customerId
} as text)`,
})
.withDimension("first_name", {
type: "string",
sql: ({ model }) => model.column("FirstName"),
});

const invoicesModel = semanticLayer
.model<QueryContext>()
.withName("invoices")
.fromTable("Invoice")
.withDimension("invoice_id", {
type: "number",
primaryKey: true,
sql: ({ model }) => model.column("InvoiceId"),
})
.withDimension("customer_id", {
type: "number",
sql: ({ model }) => model.column("CustomerId"),
});
const invoicesModel = semanticLayer
.model<QueryContext>()
.withName("invoices")
.fromTable("Invoice")
.withDimension("invoice_id", {
type: "number",
primaryKey: true,
sql: ({ model }) => model.column("InvoiceId"),
})
.withDimension("customer_id", {
type: "number",
sql: ({ model }) => model.column("CustomerId"),
});

const repository = semanticLayer
.repository<QueryContext>()
.withModel(customersModel)
.withModel(invoicesModel)
.joinOneToMany(
"customers",
"invoices",
({ sql, models, getContext }) =>
sql`${models.customers.dimension(
"customer_id",
)} = ${models.invoices.dimension("customer_id")} and ${
getContext().customerId
} = ${getContext().customerId}`,
);
const repository = semanticLayer
.repository<QueryContext>()
.withModel(customersModel)
.withModel(invoicesModel)
.joinOneToMany(
"customers",
"invoices",
({ sql, models, getContext }) =>
sql`${models.customers.dimension(
"customer_id",
)} = ${models.invoices.dimension("customer_id")} and ${
getContext().customerId
} = ${getContext().customerId}`,
);

await it("propagates context to all sql functions", async () => {
const queryBuilder = repository.build("postgresql");
const query = queryBuilder.buildQuery(
{
Expand All @@ -1961,5 +2012,41 @@ await describe("semantic layer", async () => {
// First 5 bindings are for the customerId, last one is for the limit
assert.deepEqual(query.bindings, [1, 1, 1, 1, 1, 5000]);
});

await it("propagates context to query filters", async () => {
const queryBuilder = repository.build("postgresql");
const query = queryBuilder.buildQuery(
{
dimensions: ["customers.customer_id", "invoices.invoice_id"],
filters: [
{
operator: "inQuery",
member: "customers.customer_id",
value: {
dimensions: ["customers.customer_id"],
filters: [
{
operator: "equals",
member: "customers.customer_id",
value: [1],
},
],
},
},
],
},
{ customerId: 1 },
);

assert.equal(
query.sql,
'select "q0"."customers___customer_id" as "customers___customer_id", "q0"."invoices___invoice_id" as "invoices___invoice_id" from (select "invoices_query"."customers___customer_id" as "customers___customer_id", "invoices_query"."invoices___invoice_id" as "invoices___invoice_id" from (select distinct "Invoice"."InvoiceId" as "invoices___invoice_id", "customers"."CustomerId" || cast($1 as text) as "customers___customer_id" from "Invoice" right join (select * from "Customer" where "CustomerId" = $2) as customers on "customers"."CustomerId" || cast($3 as text) = "Invoice"."CustomerId" and $4 = $5 where "customers"."CustomerId" || cast($6 as text) in (select "q0"."customers___customer_id" as "customers___customer_id" from (select "customers_query"."customers___customer_id" as "customers___customer_id" from (select distinct "customers"."CustomerId" || cast($7 as text) as "customers___customer_id" from (select * from "Customer" where "CustomerId" = $8) as customers where "customers"."CustomerId" || cast($9 as text) = $10) as "customers_query") as "q0" order by "customers___customer_id" asc limit $11)) as "invoices_query") as "q0" order by "customers___customer_id" asc limit $12',
);

assert.deepEqual(
query.bindings,
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5000, 5000],
);
});
});
});
Loading

0 comments on commit ed450b6

Please sign in to comment.