mdoTomofumi Chiba
4/9/2023, 10:43:04 PM

deno-postgres で prepared statement の query を条件分岐して組み立てる QueryBuilder を作ってみた

条件分岐しつつ prepared statement を組み立てたいというのが動機です。

まずはテストコード

const LIMIT = 10;

function build({ deptId, age }: { deptId: number; age?: number }) {
  const builder = new QueryBuilder()
    .append`SELECT * FROM emp WHERE dept_id = ${deptId}`; // (A)
  if (age) {
    builder.append`AND age > ${age}`; // (B)
  }
  builder.append(`ORDER BY DESC salary LIMIT ${LIMIT}`); // (C)
  return builder;
}

Deno.test("with age", () => {
  const builder = build({ deptId: 1, age: 50 });
  assertEquals(
    builder.query,
    "SELECT * FROM emp WHERE dept_id = $1 AND age > $2 ORDER BY DESC salary LIMIT 10",
  );
  assertEquals(builder.args, [1, 50]);
});

Deno.test("without age", () => {
  const builder = build({ deptId: 1 });
  assertEquals(
    builder.query,
    "SELECT * FROM emp WHERE dept_id = $1 ORDER BY DESC salary LIMIT 10",
  );
  assertEquals(builder.args, [1]);
});
  • (A),(B) は、タグ付きテンプレートリテラルで prepared statement の placeholder として解釈されます。
  • (C) は、通常のテンプレートリテラルで、事前に文字列として展開されます。

QueryBuilder(実装)

export class QueryBuilder {
  #index = 1;
  #query = "";
  readonly #args: unknown[] = [];

  get query() {
    return this.#query;
  }

  get args() {
    return this.#args;
  }

  append(query: TemplateStringsArray | string, ...args: unknown[]) {
    if (this.#query !== "") {
      this.#query += " ";
    }
    if (typeof query === "string") {
      // query: string
      this.#query += query;
      return this;
    }
    // query: TemplateStringsArray
    query.forEach((value, index) => {
      this.#query += value +
        ((index < query.length - 1) ? "$" + (this.#index++) : "");
    });
    this.#args.push(...args);
    return this;
  }
}
TweetBlueskyLike