pgsql-template-tag 0.0.4 → 0.0.6

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.
@@ -5,9 +5,32 @@ export type Value = unknown;
5
5
  /**
6
6
  * SQL クエリーの構築に使用できる生の値、または別の Sql インスタンスを表す型です。
7
7
  */
8
- export type RawValue = Value | Sql;
8
+ export type RawValue = Value | Slot | Sql;
9
+ /**
10
+ * スロットを表すクラスです。
11
+ *
12
+ * スロットは後から値を注入可能なプレースホルダーです。
13
+ */
14
+ export declare class Slot {
15
+ /**
16
+ * スロット名です。
17
+ */
18
+ readonly name: string;
19
+ /**
20
+ * デフォルト値です。
21
+ */
22
+ readonly defaultValue: Value;
23
+ /**
24
+ * 新しい Slot インスタンスを初期化します。
25
+ *
26
+ * @param name スロット名です。
27
+ * @param defaultValue デフォルト値です。
28
+ */
29
+ constructor(name: string, defaultValue?: Value);
30
+ }
9
31
  /**
10
32
  * 安全な SQL クエリーを構築するためのクラスです。
33
+ *
11
34
  * プレースホルダーを使用したパラメーター化クエリーを生成します。
12
35
  */
13
36
  export declare class Sql {
@@ -34,6 +57,15 @@ export declare class Sql {
34
57
  * @param rawBindings 文字列の間に挿入される値の配列です。
35
58
  */
36
59
  constructor(rawStrings: readonly string[], rawBindings: readonly RawValue[]);
60
+ /**
61
+ * スロットを値で埋めます。
62
+ *
63
+ * @param slots スロットの値です。
64
+ * @returns スロットが埋められた新しい Sql インスタンスです。
65
+ */
66
+ fill(slots: {
67
+ readonly [name: string]: Value;
68
+ } | Iterable<readonly [string | Slot, Value]>): Sql;
37
69
  /**
38
70
  * オブジェクトを JSON 形式に変換可能な形式で返します。
39
71
  *
@@ -52,6 +84,7 @@ export declare class Sql {
52
84
  }
53
85
  /**
54
86
  * 生の文字列を SQL 断片として扱います。
87
+ *
55
88
  * この値はパラメーター化の対象にならず、そのままクエリーに含まれます。
56
89
  *
57
90
  * @param value SQL に含める生の文字列です。
@@ -81,4 +114,13 @@ export declare function join(values: readonly RawValue[], separator?: string | u
81
114
  * @returns エスケープ済みの識別子文字列を返します。
82
115
  */
83
116
  export declare function ident(value: string): string;
117
+ /**
118
+ * 文字列を SQL のリテラル文字列として安全にエスケープします。
119
+ *
120
+ * 一重引用符を二重にすることでエスケープを行い、全体を一重引用符で囲みます。
121
+ *
122
+ * @param value エスケープする文字列です。
123
+ * @returns エスケープ済みのリテラル文字列を返します。
124
+ */
125
+ export declare function literal(value: string): string;
84
126
  //# sourceMappingURL=core.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../src/core.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,KAAK,GAAG,OAAO,CAAC;AAE5B;;GAEG;AAEH,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,GAAG,CAAC;AAsBnC;;;GAGG;AACH,qBAAa,GAAG;IACd;;;;;;OAMG;IACH,IAAW,IAAI,IAAI,MAAM,CAexB;IAED;;OAEG;IACH,SAAgB,MAAM,EAAE,SAAS,KAAK,EAAE,CAAC;IAEzC;;OAEG;IACH,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAgB;IAElC;;;;;OAKG;gBACgB,UAAU,EAAE,SAAS,MAAM,EAAE,EAAE,WAAW,EAAE,SAAS,QAAQ,EAAE;IAsElF;;;;OAIG;IACI,MAAM,IAAI;QACf,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,KAAK,EAAE,CAAC;KACjB;IAOD;;;;OAIG;IACI,QAAQ,IAAI,MAAM;CAG1B;AAED;;;;;;GAMG;AACH,wBAAgB,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAEtC;AAED;;GAEG;AACH,eAAO,MAAM,KAAK,EAAE,GAAa,CAAC;AAElC;;;;;;;;GAQG;AACH,wBAAgB,IAAI,CAAC,MAAM,EAAE,SAAS,QAAQ,EAAE,EAAE,SAAS,GAAE,MAAM,GAAG,SAAe,GAAG,GAAG,CAM1F;AAOD;;;;;;;GAOG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE3C"}
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../src/core.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,MAAM,KAAK,GAAG,OAAO,CAAC;AAE5B;;GAEG;AAEH,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,IAAI,GAAG,GAAG,CAAC;AAE1C;;;;GAIG;AACH,qBAAa,IAAI;IACf;;OAEG;IACH,SAAgB,IAAI,EAAE,MAAM,CAAC;IAE7B;;OAEG;IACH,SAAgB,YAAY,EAAE,KAAK,CAAC;IAEpC;;;;;OAKG;gBACgB,IAAI,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,KAAK;CAQtD;AAgCD;;;;GAIG;AACH,qBAAa,GAAG;IACd;;;;;;OAMG;IACH,IAAW,IAAI,IAAI,MAAM,CAexB;IAED;;OAEG;IACH,SAAgB,MAAM,EAAE,SAAS,KAAK,EAAE,CAAC;IAEzC;;OAEG;IACH,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAgB;IAElC;;;;;OAKG;gBACgB,UAAU,EAAE,SAAS,MAAM,EAAE,EAAE,WAAW,EAAE,SAAS,QAAQ,EAAE;IA2GlF;;;;;OAKG;IACI,IAAI,CACT,KAAK,EAAE;QAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,KAAK,CAAA;KAAE,GAAG,QAAQ,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,EAAE,KAAK,CAAC,CAAC,GACpF,GAAG;IAmCN;;;;OAIG;IACI,MAAM,IAAI;QACf,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,KAAK,EAAE,CAAC;KACjB;IAOD;;;;OAIG;IACI,QAAQ,IAAI,MAAM;CAG1B;AAED;;;;;;;GAOG;AACH,wBAAgB,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAEtC;AAED;;GAEG;AACH,eAAO,MAAM,KAAK,EAAE,GAAa,CAAC;AAElC;;;;;;;;GAQG;AACH,wBAAgB,IAAI,CAAC,MAAM,EAAE,SAAS,QAAQ,EAAE,EAAE,SAAS,GAAE,MAAM,GAAG,SAAe,GAAG,GAAG,CAM1F;AAOD;;;;;;;GAOG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE3C;AAOD;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE7C"}
package/dist/src/core.js CHANGED
@@ -1,5 +1,27 @@
1
+ import { isPlainObject } from "es-toolkit/predicate";
2
+ /**
3
+ * スロットを表すクラスです。
4
+ *
5
+ * スロットは後から値を注入可能なプレースホルダーです。
6
+ */
7
+ export class Slot {
8
+ /**
9
+ * 新しい Slot インスタンスを初期化します。
10
+ *
11
+ * @param name スロット名です。
12
+ * @param defaultValue デフォルト値です。
13
+ */
14
+ constructor(name, defaultValue) {
15
+ if (arguments.length < 2) {
16
+ defaultValue = null;
17
+ }
18
+ this.name = name;
19
+ this.defaultValue = defaultValue;
20
+ }
21
+ }
1
22
  /**
2
23
  * 安全な SQL クエリーを構築するためのクラスです。
24
+ *
3
25
  * プレースホルダーを使用したパラメーター化クエリーを生成します。
4
26
  */
5
27
  export class Sql {
@@ -12,15 +34,15 @@ export class Sql {
12
34
  */
13
35
  get text() {
14
36
  // キャッシュが存在しない場合にのみ、文字列を構築します。
15
- if (this._.t === undefined) {
16
- let i = 0, text = this._.s[0];
37
+ if (this._.text === undefined) {
38
+ let i = 0, text = this._.parts[0];
17
39
  // 文字列の断片とプレースホルダー($1, $2...)を交互に結合します。
18
- for (; i < this._.p.length; i++) {
19
- text += "$" + this._.p[i] + this._.s[i + 1];
40
+ for (; i < this._.phIds.length; i++) {
41
+ text += "$" + this._.phIds[i] + this._.parts[i + 1];
20
42
  }
21
- this._.t = text;
43
+ this._.text = text;
22
44
  }
23
- return this._.t;
45
+ return this._.text;
24
46
  }
25
47
  /**
26
48
  * 新しい Sql インスタンスを初期化します。
@@ -38,8 +60,44 @@ export class Sql {
38
60
  const strings = [rawStrings[0]];
39
61
  const bindings = [];
40
62
  const placeholderIds = [];
63
+ const idx2slot = new Map();
64
+ const slot2idx = new Map();
41
65
  /** 値の重複を排除し、同じ値には同じプレースホルダー ID を割り当てるためのマップです。 */
42
66
  const valueToId = new Map();
67
+ /** スロットごとのプレースホルダー ID を管理します。 */
68
+ const slotToId = new Map();
69
+ /**
70
+ * 値を bindings に登録し、placeholderId を取得します。
71
+ */
72
+ const registerValue = (value) => {
73
+ let placeholderId = valueToId.get(value);
74
+ if (placeholderId === undefined) {
75
+ bindings.push(value);
76
+ placeholderId = bindings.length;
77
+ valueToId.set(value, placeholderId);
78
+ }
79
+ return placeholderId;
80
+ };
81
+ /**
82
+ * スロットを bindings に登録し、placeholderId を取得します。
83
+ */
84
+ const registerSlot = (slot) => {
85
+ let placeholderId = slotToId.get(slot);
86
+ if (placeholderId === undefined) {
87
+ bindings.push(slot.defaultValue);
88
+ placeholderId = bindings.length;
89
+ slotToId.set(slot, placeholderId);
90
+ const index = placeholderId - 1;
91
+ idx2slot.set(index, slot);
92
+ let idxes = slot2idx.get(slot.name);
93
+ if (idxes === undefined) {
94
+ idxes = new Set();
95
+ slot2idx.set(slot.name, idxes);
96
+ }
97
+ idxes.add(index);
98
+ }
99
+ return placeholderId;
100
+ };
43
101
  // 提供された全てのバインディング値を走査して、SQL 文字列と値を正規化します。
44
102
  for (let i = 0; i < rawBindings.length; i++) {
45
103
  const child = rawBindings[i];
@@ -47,30 +105,22 @@ export class Sql {
47
105
  // バインディング値が Sql インスタンス(ネストされたクエリー)の場合の処理です。
48
106
  if (child instanceof Sql) {
49
107
  // 現在の最後の文字列断片に、ネストされた Sql の最初の断片を結合します。
50
- strings[strings.length - 1] += child._.s[0];
108
+ strings[strings.length - 1] += child._.parts[0];
51
109
  // ネストされた Sql のプレースホルダーと値を再マッピングします。
52
- for (let j = 0; j < child._.p.length; j++) {
53
- const childPlaceholderId = child._.p[j];
54
- const value = child.values[childPlaceholderId - 1];
55
- let placeholderId = valueToId.get(value);
56
- if (placeholderId === undefined) {
57
- bindings.push(value);
58
- placeholderId = bindings.length;
59
- valueToId.set(value, placeholderId);
60
- }
61
- strings.push(child._.s[j + 1]);
110
+ for (let j = 0; j < child._.phIds.length; j++) {
111
+ const childPlaceholderId = child._.phIds[j];
112
+ const valueIndex = childPlaceholderId - 1;
113
+ const value = child.values[valueIndex];
114
+ const slot = child._.idx2slot.get(valueIndex);
115
+ const placeholderId = slot !== undefined ? registerSlot(slot) : registerValue(value);
116
+ strings.push(child._.parts[j + 1]);
62
117
  placeholderIds.push(placeholderId);
63
118
  }
64
119
  // ネストされた Sql の展開が終わった後に、後続の生の文字列を結合します。
65
120
  strings[strings.length - 1] += rawString;
66
121
  }
67
122
  else {
68
- let placeholderId = valueToId.get(child);
69
- if (placeholderId === undefined) {
70
- bindings.push(child);
71
- placeholderId = bindings.length;
72
- valueToId.set(child, placeholderId);
73
- }
123
+ const placeholderId = child instanceof Slot ? registerSlot(child) : registerValue(child);
74
124
  strings.push(rawString);
75
125
  placeholderIds.push(placeholderId);
76
126
  }
@@ -79,11 +129,51 @@ export class Sql {
79
129
  // 内部状態を隠蔽し、不必要なプロパティーの露出を防ぎます。
80
130
  Object.defineProperty(this, "_", {
81
131
  value: {
82
- s: strings,
83
- p: placeholderIds,
132
+ parts: strings,
133
+ phIds: placeholderIds,
134
+ idx2slot,
135
+ slot2idx,
84
136
  },
85
137
  });
86
138
  }
139
+ /**
140
+ * スロットを値で埋めます。
141
+ *
142
+ * @param slots スロットの値です。
143
+ * @returns スロットが埋められた新しい Sql インスタンスです。
144
+ */
145
+ fill(slots) {
146
+ if (isPlainObject(slots)) {
147
+ slots = Object.entries(slots);
148
+ }
149
+ else {
150
+ // 一旦 Map のインスタンスにすることで、重複する名前またはスロットを 1 つに絞ります。
151
+ slots = new Map(slots);
152
+ }
153
+ const values = [...this.values];
154
+ for (const [target, value] of new Map(slots)) {
155
+ let idxes;
156
+ if (typeof target === "string") {
157
+ idxes = this._.slot2idx.get(target);
158
+ }
159
+ else {
160
+ for (const [idx, slot] of this._.idx2slot) {
161
+ if (slot === target) {
162
+ idxes || (idxes = new Set());
163
+ idxes.add(idx);
164
+ }
165
+ }
166
+ }
167
+ if (idxes === undefined) {
168
+ // スロットが見つからない場合は無視します。
169
+ continue;
170
+ }
171
+ for (const idx of idxes) {
172
+ values[idx] = value;
173
+ }
174
+ }
175
+ return new Sql(this._.parts, values);
176
+ }
87
177
  /**
88
178
  * オブジェクトを JSON 形式に変換可能な形式で返します。
89
179
  *
@@ -106,6 +196,7 @@ export class Sql {
106
196
  }
107
197
  /**
108
198
  * 生の文字列を SQL 断片として扱います。
199
+ *
109
200
  * この値はパラメーター化の対象にならず、そのままクエリーに含まれます。
110
201
  *
111
202
  * @param value SQL に含める生の文字列です。
@@ -148,3 +239,18 @@ const DOUBLE_QUOTE_REGEX = /"/g;
148
239
  export function ident(value) {
149
240
  return '"' + value.replace(DOUBLE_QUOTE_REGEX, '""') + '"';
150
241
  }
242
+ /**
243
+ * 一重引用符をエスケープするための正規表現です。
244
+ */
245
+ const SINGLE_QUOTE_REGEX = /'/g;
246
+ /**
247
+ * 文字列を SQL のリテラル文字列として安全にエスケープします。
248
+ *
249
+ * 一重引用符を二重にすることでエスケープを行い、全体を一重引用符で囲みます。
250
+ *
251
+ * @param value エスケープする文字列です。
252
+ * @returns エスケープ済みのリテラル文字列を返します。
253
+ */
254
+ export function literal(value) {
255
+ return "'" + value.replace(SINGLE_QUOTE_REGEX, "''") + "'";
256
+ }
@@ -1,3 +1,3 @@
1
- export { empty, join, raw, ident, type RawValue, Sql, type Value } from "./core.js";
1
+ export { type Value, type RawValue, Sql, Slot, empty, join, raw, ident } from "./core.js";
2
2
  export { sql } from "./sql.js";
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,QAAQ,EAAE,GAAG,EAAE,KAAK,KAAK,EAAE,MAAM,WAAW,CAAC;AAEpF,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,QAAQ,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAE1F,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC"}
package/dist/src/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { empty, join, raw, ident, Sql } from "./core.js";
1
+ export { Sql, Slot, empty, join, raw, ident } from "./core.js";
2
2
  export { sql } from "./sql.js";
package/dist/src/sql.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { join, raw, Sql, ident } from "./core.js";
1
+ import { join, raw, Sql, ident, literal, Slot } from "./core.js";
2
2
  declare namespace sql {
3
3
  /**
4
4
  * SQL クエリーの構築に使用できる生の値、または別の Sql インスタンスを表す型です。
@@ -8,11 +8,25 @@ declare namespace sql {
8
8
  * SQL クエリー内で使用される値の型定義です。
9
9
  */
10
10
  type Value = import("./core.js").Value;
11
+ /**
12
+ * スロットを表すクラスです。
13
+ *
14
+ * スロットは後から値を注入可能なプレースホルダーです。
15
+ */
16
+ type Slot = import("./core.js").Slot;
11
17
  /**
12
18
  * 安全な SQL クエリーを構築するためのクラス型です。
13
19
  */
14
20
  type Sql = import("./core.js").Sql;
15
21
  }
22
+ /**
23
+ * 新しい Slot インスタンスを作成します。
24
+ *
25
+ * @param name スロット名です。
26
+ * @param defaultValue デフォルト値です。
27
+ * @returns 作成された新しい Slot インスタンスです。
28
+ */
29
+ declare function slot(name: string, defaultValue?: sql.Value): sql.Slot;
16
30
  /**
17
31
  * テンプレートリテラルを使用して SQL クエリーを安全に構築するためのタグ関数です。
18
32
  *
@@ -26,13 +40,23 @@ declare namespace sql {
26
40
  */
27
41
  declare const sql: ((strings: TemplateStringsArray, ...bindings: readonly sql.RawValue[]) => sql.Sql) & {
28
42
  /**
29
- * Sql クラス自体への参照です。
43
+ * Sql クラスです。
30
44
  */
31
45
  readonly Sql: typeof Sql;
32
46
  /**
33
47
  * 生の文字列を SQL 断片として扱うための関数です。
34
48
  */
35
49
  readonly raw: typeof raw;
50
+ /**
51
+ * 新しい Slot インスタンスを作成する関数です。
52
+ */
53
+ readonly slot: typeof slot;
54
+ /**
55
+ * スロットを表すクラスです。
56
+ *
57
+ * スロットは後から値を注入可能なプレースホルダーです。
58
+ */
59
+ readonly Slot: typeof Slot;
36
60
  /**
37
61
  * 複数の SQL 断片を結合するための関数です。
38
62
  */
@@ -45,6 +69,10 @@ declare const sql: ((strings: TemplateStringsArray, ...bindings: readonly sql.Ra
45
69
  * 識別子(テーブル名等)を安全にエスケープするための関数です。
46
70
  */
47
71
  readonly ident: typeof ident;
72
+ /**
73
+ * 文字列を安全にエスケープするための関数です。
74
+ */
75
+ readonly literal: typeof literal;
48
76
  };
49
77
  export { sql };
50
78
  //# sourceMappingURL=sql.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../../src/sql.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAEzD,kBAAU,GAAG,CAAC;IACZ;;OAEG;IACH,KAAY,QAAQ,GAAG,OAAO,WAAW,EAAE,QAAQ,CAAC;IAEpD;;OAEG;IACH,KAAY,KAAK,GAAG,OAAO,WAAW,EAAE,KAAK,CAAC;IAE9C;;OAEG;IACH,KAAY,GAAG,GAAG,OAAO,WAAW,EAAE,GAAG,CAAC;CAC3C;AAED;;;;;;;;;;GAUG;AACH,QAAA,MAAM,GAAG,aACe,oBAAoB,eAAe,SAAS,GAAG,CAAC,QAAQ,EAAE,KAAG,GAAG,CAAC,GAAG;IAIxF;;OAEG;;IAGH;;OAEG;;IAGH;;OAEG;;IAGH;;OAEG;;IAGH;;OAEG;;CAGN,CAAC;AAEF,OAAO,EAAE,GAAG,EAAE,CAAC"}
1
+ {"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../../src/sql.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAExE,kBAAU,GAAG,CAAC;IACZ;;OAEG;IACH,KAAY,QAAQ,GAAG,OAAO,WAAW,EAAE,QAAQ,CAAC;IAEpD;;OAEG;IACH,KAAY,KAAK,GAAG,OAAO,WAAW,EAAE,KAAK,CAAC;IAE9C;;;;OAIG;IACH,KAAY,IAAI,GAAG,OAAO,WAAW,EAAE,IAAI,CAAC;IAE5C;;OAEG;IACH,KAAY,GAAG,GAAG,OAAO,WAAW,EAAE,GAAG,CAAC;CAC3C;AAED;;;;;;GAMG;AACH,iBAAS,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,GAAG,CAAC,KAAK,GAAG,GAAG,CAAC,IAAI,CAAC;AAMhE;;;;;;;;;;GAUG;AACH,QAAA,MAAM,GAAG,aACe,oBAAoB,eAAe,SAAS,GAAG,CAAC,QAAQ,EAAE,KAAG,GAAG,CAAC,GAAG;IAIxF;;OAEG;;IAGH;;OAEG;;IAGH;;OAEG;;IAGH;;;;OAIG;;IAGH;;OAEG;;IAGH;;OAEG;;IAGH;;OAEG;;IAGH;;OAEG;;CAGN,CAAC;AAEF,OAAO,EAAE,GAAG,EAAE,CAAC"}
package/dist/src/sql.js CHANGED
@@ -1,4 +1,7 @@
1
- import { empty, join, raw, Sql, ident } from "./core.js";
1
+ import { empty, join, raw, Sql, ident, literal, Slot } from "./core.js";
2
+ function slot(...args) {
3
+ return new sql.Slot(...args);
4
+ }
2
5
  /**
3
6
  * テンプレートリテラルを使用して SQL クエリーを安全に構築するためのタグ関数です。
4
7
  *
@@ -14,13 +17,23 @@ const sql = /*#__PURE__*/ Object.assign(function sql(strings, ...bindings) {
14
17
  return new Sql(strings, bindings);
15
18
  }, {
16
19
  /**
17
- * Sql クラス自体への参照です。
20
+ * Sql クラスです。
18
21
  */
19
22
  Sql,
20
23
  /**
21
24
  * 生の文字列を SQL 断片として扱うための関数です。
22
25
  */
23
26
  raw,
27
+ /**
28
+ * 新しい Slot インスタンスを作成する関数です。
29
+ */
30
+ slot,
31
+ /**
32
+ * スロットを表すクラスです。
33
+ *
34
+ * スロットは後から値を注入可能なプレースホルダーです。
35
+ */
36
+ Slot,
24
37
  /**
25
38
  * 複数の SQL 断片を結合するための関数です。
26
39
  */
@@ -33,5 +46,9 @@ const sql = /*#__PURE__*/ Object.assign(function sql(strings, ...bindings) {
33
46
  * 識別子(テーブル名等)を安全にエスケープするための関数です。
34
47
  */
35
48
  ident,
49
+ /**
50
+ * 文字列を安全にエスケープするための関数です。
51
+ */
52
+ literal,
36
53
  });
37
54
  export { sql };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgsql-template-tag",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "",
5
5
  "homepage": "https://github.com/tai-kun/pgsql-template-tag",
6
6
  "license": "MIT",
@@ -25,6 +25,9 @@
25
25
  "default": "./dist/src/index.js"
26
26
  }
27
27
  },
28
+ "dependencies": {
29
+ "es-toolkit": "^1.46.1"
30
+ },
28
31
  "devDependencies": {
29
32
  "@tsconfig/node24": "^24.0.4",
30
33
  "@tsconfig/strictest": "^2.0.8",
@@ -33,9 +36,9 @@
33
36
  "@vitest/browser": "^4.1.5",
34
37
  "@vitest/browser-playwright": "^4.1.5",
35
38
  "npm-check-updates": "^20.0.2",
36
- "oxfmt": "^0.46.0",
37
- "oxlint": "^1.61.0",
38
- "oxlint-tsgolint": "^0.22.0",
39
+ "oxfmt": "^0.47.0",
40
+ "oxlint": "^1.62.0",
41
+ "oxlint-tsgolint": "^0.22.1",
39
42
  "playwright": "^1.59.1",
40
43
  "typescript": "^6.0.3",
41
44
  "vite": "^8.0.10",
package/src/core.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { isPlainObject } from "es-toolkit/predicate";
2
+
1
3
  /**
2
4
  * SQL クエリー内で使用される値の型定義です。
3
5
  */
@@ -7,7 +9,39 @@ export type Value = unknown;
7
9
  * SQL クエリーの構築に使用できる生の値、または別の Sql インスタンスを表す型です。
8
10
  */
9
11
  // oxlint-disable-next-line typescript/no-redundant-type-constituents
10
- export type RawValue = Value | Sql;
12
+ export type RawValue = Value | Slot | Sql;
13
+
14
+ /**
15
+ * スロットを表すクラスです。
16
+ *
17
+ * スロットは後から値を注入可能なプレースホルダーです。
18
+ */
19
+ export class Slot {
20
+ /**
21
+ * スロット名です。
22
+ */
23
+ public readonly name: string;
24
+
25
+ /**
26
+ * デフォルト値です。
27
+ */
28
+ public readonly defaultValue: Value;
29
+
30
+ /**
31
+ * 新しい Slot インスタンスを初期化します。
32
+ *
33
+ * @param name スロット名です。
34
+ * @param defaultValue デフォルト値です。
35
+ */
36
+ public constructor(name: string, defaultValue?: Value) {
37
+ if (arguments.length < 2) {
38
+ defaultValue = null;
39
+ }
40
+
41
+ this.name = name;
42
+ this.defaultValue = defaultValue;
43
+ }
44
+ }
11
45
 
12
46
  /**
13
47
  * Sql クラスの内部状態を管理するためのプライベートな型定義です。
@@ -16,21 +50,32 @@ type PrivateState = {
16
50
  /**
17
51
  * クエリーを構成する静的な文字列の配列です。
18
52
  */
19
- readonly s: readonly [string, ...string[]];
53
+ readonly parts: readonly [string, ...string[]];
20
54
 
21
55
  /**
22
56
  * プレースホルダーに対応するインデックス(1 から始まる数値)の配列です。
23
57
  */
24
- readonly p: readonly number[];
58
+ readonly phIds: readonly number[];
59
+
60
+ /**
61
+ * values のインデックス -> スロット情報 のマップです。
62
+ */
63
+ readonly idx2slot: ReadonlyMap<number, Slot>;
64
+
65
+ /**
66
+ * スロット名 -> values のインデックス のマップです。
67
+ */
68
+ readonly slot2idx: ReadonlyMap<string, ReadonlySet<number>>;
25
69
 
26
70
  /**
27
71
  * キャッシュされた最終的なクエリーテキストです。
28
72
  */
29
- t?: string;
73
+ text?: string;
30
74
  };
31
75
 
32
76
  /**
33
77
  * 安全な SQL クエリーを構築するためのクラスです。
78
+ *
34
79
  * プレースホルダーを使用したパラメーター化クエリーを生成します。
35
80
  */
36
81
  export class Sql {
@@ -43,19 +88,19 @@ export class Sql {
43
88
  */
44
89
  public get text(): string {
45
90
  // キャッシュが存在しない場合にのみ、文字列を構築します。
46
- if (this._.t === undefined) {
91
+ if (this._.text === undefined) {
47
92
  let i = 0,
48
- text = this._.s[0];
93
+ text = this._.parts[0];
49
94
 
50
95
  // 文字列の断片とプレースホルダー($1, $2...)を交互に結合します。
51
- for (; i < this._.p.length; i++) {
52
- text += "$" + this._.p[i] + this._.s[i + 1];
96
+ for (; i < this._.phIds.length; i++) {
97
+ text += "$" + this._.phIds[i] + this._.parts[i + 1];
53
98
  }
54
99
 
55
- this._.t = text;
100
+ this._.text = text;
56
101
  }
57
102
 
58
- return this._.t;
103
+ return this._.text;
59
104
  }
60
105
 
61
106
  /**
@@ -88,10 +133,52 @@ export class Sql {
88
133
  const strings: [string, ...string[]] = [rawStrings[0]!];
89
134
  const bindings: Value[] = [];
90
135
  const placeholderIds: number[] = [];
136
+ const idx2slot = new Map<number, Slot>();
137
+ const slot2idx = new Map<string, Set<number>>();
91
138
 
92
139
  /** 値の重複を排除し、同じ値には同じプレースホルダー ID を割り当てるためのマップです。 */
93
140
  const valueToId = new Map<Value, number>();
94
141
 
142
+ /** スロットごとのプレースホルダー ID を管理します。 */
143
+ const slotToId = new Map<Slot, number>();
144
+
145
+ /**
146
+ * 値を bindings に登録し、placeholderId を取得します。
147
+ */
148
+ const registerValue = (value: Value): number => {
149
+ let placeholderId = valueToId.get(value);
150
+ if (placeholderId === undefined) {
151
+ bindings.push(value);
152
+ placeholderId = bindings.length;
153
+ valueToId.set(value, placeholderId);
154
+ }
155
+
156
+ return placeholderId;
157
+ };
158
+
159
+ /**
160
+ * スロットを bindings に登録し、placeholderId を取得します。
161
+ */
162
+ const registerSlot = (slot: Slot): number => {
163
+ let placeholderId = slotToId.get(slot);
164
+ if (placeholderId === undefined) {
165
+ bindings.push(slot.defaultValue);
166
+ placeholderId = bindings.length;
167
+ slotToId.set(slot, placeholderId);
168
+
169
+ const index = placeholderId - 1;
170
+ idx2slot.set(index, slot);
171
+ let idxes = slot2idx.get(slot.name);
172
+ if (idxes === undefined) {
173
+ idxes = new Set();
174
+ slot2idx.set(slot.name, idxes);
175
+ }
176
+ idxes.add(index);
177
+ }
178
+
179
+ return placeholderId;
180
+ };
181
+
95
182
  // 提供された全てのバインディング値を走査して、SQL 文字列と値を正規化します。
96
183
  for (let i = 0; i < rawBindings.length; i++) {
97
184
  const child = rawBindings[i];
@@ -100,33 +187,26 @@ export class Sql {
100
187
  // バインディング値が Sql インスタンス(ネストされたクエリー)の場合の処理です。
101
188
  if (child instanceof Sql) {
102
189
  // 現在の最後の文字列断片に、ネストされた Sql の最初の断片を結合します。
103
- strings[strings.length - 1] += child._.s[0];
190
+ strings[strings.length - 1] += child._.parts[0];
104
191
 
105
192
  // ネストされた Sql のプレースホルダーと値を再マッピングします。
106
- for (let j = 0; j < child._.p.length; j++) {
107
- const childPlaceholderId = child._.p[j]!;
108
- const value = child.values[childPlaceholderId - 1]!;
109
-
110
- let placeholderId = valueToId.get(value);
111
- if (placeholderId === undefined) {
112
- bindings.push(value);
113
- placeholderId = bindings.length;
114
- valueToId.set(value, placeholderId);
115
- }
193
+ for (let j = 0; j < child._.phIds.length; j++) {
194
+ const childPlaceholderId = child._.phIds[j]!;
195
+ const valueIndex = childPlaceholderId - 1;
196
+ const value = child.values[valueIndex]!;
197
+
198
+ const slot = child._.idx2slot.get(valueIndex);
199
+
200
+ const placeholderId = slot !== undefined ? registerSlot(slot) : registerValue(value);
116
201
 
117
- strings.push(child._.s[j + 1]!);
202
+ strings.push(child._.parts[j + 1]!);
118
203
  placeholderIds.push(placeholderId);
119
204
  }
120
205
 
121
206
  // ネストされた Sql の展開が終わった後に、後続の生の文字列を結合します。
122
207
  strings[strings.length - 1] += rawString;
123
208
  } else {
124
- let placeholderId = valueToId.get(child);
125
- if (placeholderId === undefined) {
126
- bindings.push(child);
127
- placeholderId = bindings.length;
128
- valueToId.set(child, placeholderId);
129
- }
209
+ const placeholderId = child instanceof Slot ? registerSlot(child) : registerValue(child);
130
210
 
131
211
  strings.push(rawString);
132
212
  placeholderIds.push(placeholderId);
@@ -138,12 +218,57 @@ export class Sql {
138
218
  // 内部状態を隠蔽し、不必要なプロパティーの露出を防ぎます。
139
219
  Object.defineProperty(this, "_", {
140
220
  value: {
141
- s: strings,
142
- p: placeholderIds,
143
- },
221
+ parts: strings,
222
+ phIds: placeholderIds,
223
+ idx2slot,
224
+ slot2idx,
225
+ } satisfies PrivateState,
144
226
  });
145
227
  }
146
228
 
229
+ /**
230
+ * スロットを値で埋めます。
231
+ *
232
+ * @param slots スロットの値です。
233
+ * @returns スロットが埋められた新しい Sql インスタンスです。
234
+ */
235
+ public fill(
236
+ slots: { readonly [name: string]: Value } | Iterable<readonly [string | Slot, Value]>,
237
+ ): Sql {
238
+ if (isPlainObject(slots)) {
239
+ slots = Object.entries(slots);
240
+ } else {
241
+ // 一旦 Map のインスタンスにすることで、重複する名前またはスロットを 1 つに絞ります。
242
+ slots = new Map(slots);
243
+ }
244
+
245
+ const values: Value[] = [...this.values];
246
+ for (const [target, value] of new Map(slots)) {
247
+ let idxes: ReadonlySet<number> | undefined;
248
+ if (typeof target === "string") {
249
+ idxes = this._.slot2idx.get(target);
250
+ } else {
251
+ for (const [idx, slot] of this._.idx2slot) {
252
+ if (slot === target) {
253
+ idxes ||= new Set();
254
+ (idxes as Set<number>).add(idx);
255
+ }
256
+ }
257
+ }
258
+
259
+ if (idxes === undefined) {
260
+ // スロットが見つからない場合は無視します。
261
+ continue;
262
+ }
263
+
264
+ for (const idx of idxes) {
265
+ values[idx] = value;
266
+ }
267
+ }
268
+
269
+ return new Sql(this._.parts, values);
270
+ }
271
+
147
272
  /**
148
273
  * オブジェクトを JSON 形式に変換可能な形式で返します。
149
274
  *
@@ -171,6 +296,7 @@ export class Sql {
171
296
 
172
297
  /**
173
298
  * 生の文字列を SQL 断片として扱います。
299
+ *
174
300
  * この値はパラメーター化の対象にならず、そのままクエリーに含まれます。
175
301
  *
176
302
  * @param value SQL に含める生の文字列です。
@@ -218,3 +344,20 @@ const DOUBLE_QUOTE_REGEX = /"/g;
218
344
  export function ident(value: string): string {
219
345
  return '"' + value.replace(DOUBLE_QUOTE_REGEX, '""') + '"';
220
346
  }
347
+
348
+ /**
349
+ * 一重引用符をエスケープするための正規表現です。
350
+ */
351
+ const SINGLE_QUOTE_REGEX = /'/g;
352
+
353
+ /**
354
+ * 文字列を SQL のリテラル文字列として安全にエスケープします。
355
+ *
356
+ * 一重引用符を二重にすることでエスケープを行い、全体を一重引用符で囲みます。
357
+ *
358
+ * @param value エスケープする文字列です。
359
+ * @returns エスケープ済みのリテラル文字列を返します。
360
+ */
361
+ export function literal(value: string): string {
362
+ return "'" + value.replace(SINGLE_QUOTE_REGEX, "''") + "'";
363
+ }
package/src/index.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { empty, join, raw, ident, type RawValue, Sql, type Value } from "./core.js";
1
+ export { type Value, type RawValue, Sql, Slot, empty, join, raw, ident } from "./core.js";
2
2
 
3
3
  export { sql } from "./sql.js";
package/src/sql.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { empty, join, raw, Sql, ident } from "./core.js";
1
+ import { empty, join, raw, Sql, ident, literal, Slot } from "./core.js";
2
2
 
3
3
  namespace sql {
4
4
  /**
@@ -11,12 +11,32 @@ namespace sql {
11
11
  */
12
12
  export type Value = import("./core.js").Value;
13
13
 
14
+ /**
15
+ * スロットを表すクラスです。
16
+ *
17
+ * スロットは後から値を注入可能なプレースホルダーです。
18
+ */
19
+ export type Slot = import("./core.js").Slot;
20
+
14
21
  /**
15
22
  * 安全な SQL クエリーを構築するためのクラス型です。
16
23
  */
17
24
  export type Sql = import("./core.js").Sql;
18
25
  }
19
26
 
27
+ /**
28
+ * 新しい Slot インスタンスを作成します。
29
+ *
30
+ * @param name スロット名です。
31
+ * @param defaultValue デフォルト値です。
32
+ * @returns 作成された新しい Slot インスタンスです。
33
+ */
34
+ function slot(name: string, defaultValue?: sql.Value): sql.Slot;
35
+
36
+ function slot(...args: [any]): sql.Slot {
37
+ return new sql.Slot(...args);
38
+ }
39
+
20
40
  /**
21
41
  * テンプレートリテラルを使用して SQL クエリーを安全に構築するためのタグ関数です。
22
42
  *
@@ -34,7 +54,7 @@ const sql = /*#__PURE__*/ Object.assign(
34
54
  },
35
55
  {
36
56
  /**
37
- * Sql クラス自体への参照です。
57
+ * Sql クラスです。
38
58
  */
39
59
  Sql,
40
60
 
@@ -43,6 +63,18 @@ const sql = /*#__PURE__*/ Object.assign(
43
63
  */
44
64
  raw,
45
65
 
66
+ /**
67
+ * 新しい Slot インスタンスを作成する関数です。
68
+ */
69
+ slot,
70
+
71
+ /**
72
+ * スロットを表すクラスです。
73
+ *
74
+ * スロットは後から値を注入可能なプレースホルダーです。
75
+ */
76
+ Slot,
77
+
46
78
  /**
47
79
  * 複数の SQL 断片を結合するための関数です。
48
80
  */
@@ -57,6 +89,11 @@ const sql = /*#__PURE__*/ Object.assign(
57
89
  * 識別子(テーブル名等)を安全にエスケープするための関数です。
58
90
  */
59
91
  ident,
92
+
93
+ /**
94
+ * 文字列を安全にエスケープするための関数です。
95
+ */
96
+ literal,
60
97
  } as const,
61
98
  );
62
99