pgsql-template-tag 0.0.5 → 0.0.7

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.
@@ -1,3 +1,4 @@
1
+ import type { UnionToTuple } from "type-fest";
1
2
  /**
2
3
  * SQL クエリー内で使用される値の型定義です。
3
4
  */
@@ -5,12 +6,123 @@ export type Value = unknown;
5
6
  /**
6
7
  * SQL クエリーの構築に使用できる生の値、または別の Sql インスタンスを表す型です。
7
8
  */
8
- export type RawValue = Value | Sql;
9
+ export type RawValue = Value | Slot | Sql;
10
+ /**
11
+ * Slot クラスを一意に識別するためのシンボルです。
12
+ */
13
+ declare const SLOT_SYMBOL: unique symbol;
14
+ /**
15
+ * Slot クラスの基底となる型定義です。
16
+ */
17
+ declare const SlotTypes: {
18
+ new (): {
19
+ /**
20
+ * このプロパティーは、TypeScript の `extends Slot` で `Slot` インスタンスのみに一致させるためにあります。そのため、`Slot` と同じプロパティーを持つオブジェクトに対して一致することはありません。
21
+ */
22
+ readonly ["~kind"]: typeof SLOT_SYMBOL;
23
+ };
24
+ };
25
+ /**
26
+ * スロットを表すクラスです。
27
+ *
28
+ * スロットは後から値を注入可能なプレースホルダーです。
29
+ *
30
+ * @template TName スロットの名前となる文字列リテラル型です。
31
+ * @template TValue スロットに許容される値の型です。
32
+ */
33
+ export declare class Slot<const TName extends string = string, TValue extends RawValue = RawValue> extends SlotTypes {
34
+ /**
35
+ * スロット名です。
36
+ */
37
+ readonly name: TName;
38
+ /**
39
+ * デフォルト値です。
40
+ */
41
+ readonly defaultValue: TValue;
42
+ /**
43
+ * 新しい Slot インスタンスを初期化します。
44
+ *
45
+ * @param name スロット名です。
46
+ * @param defaultValue デフォルト値です。
47
+ */
48
+ constructor(...args: null extends TValue ? [name: TName, defaultValue?: TValue] : [name: TName, defaultValue: TValue]);
49
+ }
50
+ /**
51
+ * オブジェクトのバリューの型を抽出するヘルパー型です。
52
+ *
53
+ * @template T 対象となるオブジェクト型です。
54
+ */
55
+ type $ValueOf<T> = T[keyof T];
56
+ /**
57
+ * スロットの配列から、再帰的に値をマージして型を決定します。
58
+ *
59
+ * @template TSlots スロットのタプル型です。
60
+ */
61
+ type $MergeSlotValue<TSlots> = TSlots extends [
62
+ Slot<string, infer TValue>,
63
+ infer TSlot,
64
+ ...infer TOtherSlots
65
+ ] ? TValue & $MergeSlotValue<[TSlot, ...TOtherSlots]> : TSlots extends [Slot<string, infer TValue>] ? TValue : never;
66
+ /**
67
+ * RawValue の配列からスロット情報を抽出し、名前ごとのマップ型に変換します。
68
+ *
69
+ * @template TValues RawValue の読み取り専用配列型です。
70
+ */
71
+ type $MapSlotValue<TValues extends readonly RawValue[]> = TValues extends readonly (infer TSlot extends Slot)[] ? {
72
+ [TName in TSlot["name"]]: $MergeSlotValue<UnionToTuple<Extract<TSlot, Slot<TName>>>>;
73
+ } : {};
74
+ /**
75
+ * 指定された値のリストに対して、スロットを実際の内容で置き換えた型を生成します。
76
+ *
77
+ * @template TValues 置き換え対象の配列型です。
78
+ * @template TSlots スロット名と値のマップ型です。
79
+ */
80
+ type $FillSlots<TValues, TSlots> = TValues extends [infer TValue, ...infer TOtherValues] ? [
81
+ TValue extends Slot<infer TName extends Extract<keyof TSlots, string>, infer TSlotValue> ? TSlotValue extends TSlots[TName] ? TSlotValue : TValue : TValue,
82
+ ...$FillSlots<TOtherValues, TSlots>
83
+ ] : [];
84
+ /**
85
+ * スロットを埋めるための部分的な引数型を定義します。
86
+ *
87
+ * @template TSlots スロット名と値のマップ型です。
88
+ */
89
+ type _FillSlots<TSlots> = {
90
+ readonly [TName in Extract<keyof TSlots, string>]?: TSlots[TName];
91
+ } | Iterable<readonly [
92
+ name: $ValueOf<{
93
+ [TName in Extract<keyof TSlots, string>]: TName | Slot<TName, TSlots[TName]>;
94
+ }>,
95
+ value: $ValueOf<TSlots>
96
+ ]>;
97
+ /**
98
+ * すべてのスロットを埋めるために必要な引数型を定義します。
99
+ *
100
+ * @template TSlots スロット名と値のマップ型です。
101
+ */
102
+ type _FillAllSlots<TSlots> = {
103
+ readonly [TName in Extract<keyof TSlots, string>]: TSlots[TName];
104
+ };
105
+ /**
106
+ * スロットの部分的な補完に使用する外部向けの型定義です。
107
+ *
108
+ * @template TValues RawValue の配列です。
109
+ */
110
+ export type FillSlots<TValues extends readonly RawValue[] = readonly RawValue[]> = _FillSlots<$MapSlotValue<TValues>>;
111
+ /**
112
+ * すべてのスロットの強制的な補完に使用する外部向けの型定義です。
113
+ *
114
+ * @template TValues RawValue の配列です。
115
+ */
116
+ export type FillAllSlots<TValues extends readonly RawValue[] = readonly RawValue[]> = _FillAllSlots<$MapSlotValue<TValues>>;
9
117
  /**
10
118
  * 安全な SQL クエリーを構築するためのクラスです。
119
+ *
11
120
  * プレースホルダーを使用したパラメーター化クエリーを生成します。
121
+ *
122
+ * @template TRawBindings クエリーに渡される生の値のタプル型です。
12
123
  */
13
- export declare class Sql {
124
+ export declare class Sql<const TRawBindings extends readonly RawValue[] = readonly RawValue[]> {
125
+ #private;
14
126
  /**
15
127
  * 構築された SQL クエリーテキストを取得します。
16
128
  *
@@ -23,17 +135,29 @@ export declare class Sql {
23
135
  * クエリーに使用されるパラメーター値の配列です。
24
136
  */
25
137
  readonly values: readonly Value[];
26
- /**
27
- * 内部状態を保持するためのプロパティーです。
28
- */
29
- private readonly _;
30
138
  /**
31
139
  * 新しい Sql インスタンスを初期化します。
32
140
  *
33
141
  * @param rawStrings SQL の断片となる文字列の配列です。
34
142
  * @param rawBindings 文字列の間に挿入される値の配列です。
35
143
  */
36
- constructor(rawStrings: readonly string[], rawBindings: readonly RawValue[]);
144
+ constructor(rawStrings: readonly string[], rawBindings: TRawBindings);
145
+ /**
146
+ * スロットを値で埋めます。
147
+ *
148
+ * @template TSlots 指定されたスロットのマップ型です。
149
+ * @param slots スロットの値です。
150
+ * @returns スロットが埋められた新しい Sql インスタンスです。
151
+ */
152
+ fill<TSlots extends FillSlots<TRawBindings>>(slots: TSlots): Sql<$FillSlots<TRawBindings, TSlots>>;
153
+ /**
154
+ * すべてのスロットを値で埋めます。
155
+ *
156
+ * @template TSlots 全てのスロットをカバーするマップ型です。
157
+ * @param slots スロットの値です。
158
+ * @returns スロットが埋められた新しい Sql インスタンスです。
159
+ */
160
+ fillAll<TSlots extends FillAllSlots<TRawBindings>>(slots: TSlots): Sql<$FillSlots<TRawBindings, TSlots>>;
37
161
  /**
38
162
  * オブジェクトを JSON 形式に変換可能な形式で返します。
39
163
  *
@@ -52,6 +176,7 @@ export declare class Sql {
52
176
  }
53
177
  /**
54
178
  * 生の文字列を SQL 断片として扱います。
179
+ *
55
180
  * この値はパラメーター化の対象にならず、そのままクエリーに含まれます。
56
181
  *
57
182
  * @param value SQL に含める生の文字列です。
@@ -90,4 +215,5 @@ export declare function ident(value: string): string;
90
215
  * @returns エスケープ済みのリテラル文字列を返します。
91
216
  */
92
217
  export declare function literal(value: string): string;
218
+ export {};
93
219
  //# 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;AAOD;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE7C"}
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../src/core.ts"],"names":[],"mappings":"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"}
package/dist/src/core.js CHANGED
@@ -1,6 +1,45 @@
1
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
2
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
3
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
4
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
5
+ };
6
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
7
+ if (kind === "m") throw new TypeError("Private method is not writable");
8
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
9
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
10
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
11
+ };
12
+ var _Sql_instances, _a, _Sql_state, _Sql_fill;
13
+ import { isPlainObject } from "es-toolkit/predicate";
14
+ /**
15
+ * Slot クラスの基底となる型定義です。
16
+ */
17
+ const SlotTypes = class {
18
+ };
19
+ /**
20
+ * スロットを表すクラスです。
21
+ *
22
+ * スロットは後から値を注入可能なプレースホルダーです。
23
+ *
24
+ * @template TName スロットの名前となる文字列リテラル型です。
25
+ * @template TValue スロットに許容される値の型です。
26
+ */
27
+ export class Slot extends SlotTypes {
28
+ constructor(name, defaultValue) {
29
+ super();
30
+ if (arguments.length < 2) {
31
+ defaultValue = null;
32
+ }
33
+ this.name = name;
34
+ this.defaultValue = defaultValue;
35
+ }
36
+ }
1
37
  /**
2
38
  * 安全な SQL クエリーを構築するためのクラスです。
39
+ *
3
40
  * プレースホルダーを使用したパラメーター化クエリーを生成します。
41
+ *
42
+ * @template TRawBindings クエリーに渡される生の値のタプル型です。
4
43
  */
5
44
  export class Sql {
6
45
  /**
@@ -12,15 +51,15 @@ export class Sql {
12
51
  */
13
52
  get text() {
14
53
  // キャッシュが存在しない場合にのみ、文字列を構築します。
15
- if (this._.t === undefined) {
16
- let i = 0, text = this._.s[0];
54
+ if (__classPrivateFieldGet(this, _Sql_state, "f").text === undefined) {
55
+ let i = 0, text = __classPrivateFieldGet(this, _Sql_state, "f").parts[0];
17
56
  // 文字列の断片とプレースホルダー($1, $2...)を交互に結合します。
18
- for (; i < this._.p.length; i++) {
19
- text += "$" + this._.p[i] + this._.s[i + 1];
57
+ for (; i < __classPrivateFieldGet(this, _Sql_state, "f").phIds.length; i++) {
58
+ text += "$" + __classPrivateFieldGet(this, _Sql_state, "f").phIds[i] + __classPrivateFieldGet(this, _Sql_state, "f").parts[i + 1];
20
59
  }
21
- this._.t = text;
60
+ __classPrivateFieldGet(this, _Sql_state, "f").text = text;
22
61
  }
23
- return this._.t;
62
+ return __classPrivateFieldGet(this, _Sql_state, "f").text;
24
63
  }
25
64
  /**
26
65
  * 新しい Sql インスタンスを初期化します。
@@ -29,6 +68,11 @@ export class Sql {
29
68
  * @param rawBindings 文字列の間に挿入される値の配列です。
30
69
  */
31
70
  constructor(rawStrings, rawBindings) {
71
+ _Sql_instances.add(this);
72
+ /**
73
+ * 内部状態を保持するためのプロパティーです。
74
+ */
75
+ _Sql_state.set(this, void 0);
32
76
  if (rawStrings.length === 0) {
33
77
  throw new TypeError("Expected at least 1 string");
34
78
  }
@@ -38,51 +82,98 @@ export class Sql {
38
82
  const strings = [rawStrings[0]];
39
83
  const bindings = [];
40
84
  const placeholderIds = [];
85
+ const idx2slot = new Map();
86
+ const slot2idx = new Map();
41
87
  /** 値の重複を排除し、同じ値には同じプレースホルダー ID を割り当てるためのマップです。 */
42
88
  const valueToId = new Map();
89
+ /** スロットごとのプレースホルダー ID を管理します。 */
90
+ const slotToId = new Map();
91
+ /**
92
+ * 値を bindings に登録し、placeholderId を取得します。
93
+ */
94
+ const registerValue = (value) => {
95
+ let placeholderId = valueToId.get(value);
96
+ if (placeholderId === undefined) {
97
+ bindings.push(value);
98
+ placeholderId = bindings.length;
99
+ valueToId.set(value, placeholderId);
100
+ }
101
+ return placeholderId;
102
+ };
103
+ /**
104
+ * スロットを bindings に登録し、placeholderId を取得します。
105
+ */
106
+ const registerSlot = (slot) => {
107
+ let placeholderId = slotToId.get(slot);
108
+ if (placeholderId === undefined) {
109
+ bindings.push(slot.defaultValue);
110
+ placeholderId = bindings.length;
111
+ slotToId.set(slot, placeholderId);
112
+ const index = placeholderId - 1;
113
+ idx2slot.set(index, slot);
114
+ let idxes = slot2idx.get(slot.name);
115
+ if (idxes === undefined) {
116
+ idxes = new Set();
117
+ slot2idx.set(slot.name, idxes);
118
+ }
119
+ idxes.add(index);
120
+ }
121
+ return placeholderId;
122
+ };
43
123
  // 提供された全てのバインディング値を走査して、SQL 文字列と値を正規化します。
44
124
  for (let i = 0; i < rawBindings.length; i++) {
45
125
  const child = rawBindings[i];
46
126
  const rawString = rawStrings[i + 1];
47
127
  // バインディング値が Sql インスタンス(ネストされたクエリー)の場合の処理です。
48
- if (child instanceof Sql) {
128
+ if (child instanceof _a) {
49
129
  // 現在の最後の文字列断片に、ネストされた Sql の最初の断片を結合します。
50
- strings[strings.length - 1] += child._.s[0];
130
+ strings[strings.length - 1] += __classPrivateFieldGet(child, _Sql_state, "f").parts[0];
51
131
  // ネストされた 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]);
132
+ for (let j = 0; j < __classPrivateFieldGet(child, _Sql_state, "f").phIds.length; j++) {
133
+ const childPlaceholderId = __classPrivateFieldGet(child, _Sql_state, "f").phIds[j];
134
+ const valueIndex = childPlaceholderId - 1;
135
+ const value = child.values[valueIndex];
136
+ const slot = __classPrivateFieldGet(child, _Sql_state, "f").idx2slot.get(valueIndex);
137
+ const placeholderId = slot !== undefined ? registerSlot(slot) : registerValue(value);
138
+ strings.push(__classPrivateFieldGet(child, _Sql_state, "f").parts[j + 1]);
62
139
  placeholderIds.push(placeholderId);
63
140
  }
64
141
  // ネストされた Sql の展開が終わった後に、後続の生の文字列を結合します。
65
142
  strings[strings.length - 1] += rawString;
66
143
  }
67
144
  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
- }
145
+ const placeholderId = child instanceof Slot ? registerSlot(child) : registerValue(child);
74
146
  strings.push(rawString);
75
147
  placeholderIds.push(placeholderId);
76
148
  }
77
149
  }
78
150
  this.values = bindings;
79
- // 内部状態を隠蔽し、不必要なプロパティーの露出を防ぎます。
80
- Object.defineProperty(this, "_", {
81
- value: {
82
- s: strings,
83
- p: placeholderIds,
84
- },
85
- });
151
+ __classPrivateFieldSet(this, _Sql_state, {
152
+ parts: strings,
153
+ phIds: placeholderIds,
154
+ idx2slot,
155
+ slot2idx,
156
+ }, "f");
157
+ }
158
+ /**
159
+ * スロットを値で埋めます。
160
+ *
161
+ * @template TSlots 指定されたスロットのマップ型です。
162
+ * @param slots スロットの値です。
163
+ * @returns スロットが埋められた新しい Sql インスタンスです。
164
+ */
165
+ fill(slots) {
166
+ return __classPrivateFieldGet(this, _Sql_instances, "m", _Sql_fill).call(this, slots, false);
167
+ }
168
+ /**
169
+ * すべてのスロットを値で埋めます。
170
+ *
171
+ * @template TSlots 全てのスロットをカバーするマップ型です。
172
+ * @param slots スロットの値です。
173
+ * @returns スロットが埋められた新しい Sql インスタンスです。
174
+ */
175
+ fillAll(slots) {
176
+ return __classPrivateFieldGet(this, _Sql_instances, "m", _Sql_fill).call(this, slots, true);
86
177
  }
87
178
  /**
88
179
  * オブジェクトを JSON 形式に変換可能な形式で返します。
@@ -104,8 +195,61 @@ export class Sql {
104
195
  return this.text;
105
196
  }
106
197
  }
198
+ _a = Sql, _Sql_state = new WeakMap(), _Sql_instances = new WeakSet(), _Sql_fill = function _Sql_fill(slots, all) {
199
+ if (isPlainObject(slots)) {
200
+ slots = Object.entries(slots);
201
+ }
202
+ else {
203
+ // 一旦 Map のインスタンスにすることで、重複する名前またはスロットを 1 つに絞ります。
204
+ slots = new Map(slots);
205
+ }
206
+ const { parts, idx2slot, slot2idx } = __classPrivateFieldGet(this, _Sql_state, "f");
207
+ const filled = new Set();
208
+ const values = this.values.slice();
209
+ for (const [target, value] of new Map(slots)) {
210
+ let idxes;
211
+ if (typeof target === "string") {
212
+ idxes = slot2idx.get(target);
213
+ }
214
+ else {
215
+ // インスタンスが直接指定された場合、全インデックスから一致するものを探します。
216
+ for (const [idx, slot] of idx2slot) {
217
+ if (slot === target) {
218
+ idxes || (idxes = new Set());
219
+ idxes.add(idx);
220
+ }
221
+ }
222
+ }
223
+ if (idxes === undefined) {
224
+ // スロットが見つからない場合は無視します。
225
+ continue;
226
+ }
227
+ // 該当するすべてのプレースホルダーインデックスを新しい値で更新します。
228
+ for (const idx of idxes) {
229
+ values[idx] = value;
230
+ filled.add(idx);
231
+ }
232
+ }
233
+ // 全て埋める必要がある場合、未解決のスロットが残っていないか検証します。
234
+ if (all) {
235
+ for (let idx = 0; idx < values.length; idx++) {
236
+ if (idx2slot.has(idx) && !filled.has(idx)) {
237
+ const missingSlots = new Set();
238
+ missingSlots.add(idx2slot.get(idx).name);
239
+ for (; idx < values.length; idx++) {
240
+ if (idx2slot.has(idx)) {
241
+ missingSlots.add(idx2slot.get(idx).name);
242
+ }
243
+ }
244
+ throw new Error(`Not all slots are filled. Missing: ${[...missingSlots].join(", ")}`);
245
+ }
246
+ }
247
+ }
248
+ return new _a(parts, values);
249
+ };
107
250
  /**
108
251
  * 生の文字列を SQL 断片として扱います。
252
+ *
109
253
  * この値はパラメーター化の対象にならず、そのままクエリーに含まれます。
110
254
  *
111
255
  * @param value SQL に含める生の文字列です。
@@ -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, literal } from "./core.js";
1
+ import { join, raw, Sql, ident, literal, Slot, RawValue } from "./core.js";
2
2
  declare namespace sql {
3
3
  /**
4
4
  * SQL クエリーの構築に使用できる生の値、または別の Sql インスタンスを表す型です。
@@ -8,14 +8,36 @@ declare namespace sql {
8
8
  * SQL クエリー内で使用される値の型定義です。
9
9
  */
10
10
  type Value = import("./core.js").Value;
11
+ /**
12
+ * スロットを表すクラスです。
13
+ *
14
+ * スロットは後から値を注入可能なプレースホルダーです。
15
+ *
16
+ * @template TName スロットの名前となる文字列リテラル型です。
17
+ * @template TValue スロットに許容される値の型です。
18
+ */
19
+ type Slot<TName extends string = string, TValue extends RawValue = RawValue> = import("./core.js").Slot<TName, TValue>;
11
20
  /**
12
21
  * 安全な SQL クエリーを構築するためのクラス型です。
22
+ *
23
+ * @template TRawBindings クエリーに渡される生の値のタプル型です。
13
24
  */
14
- type Sql = import("./core.js").Sql;
25
+ type Sql<TRawBindings extends readonly RawValue[] = readonly RawValue[]> = import("./core.js").Sql<TRawBindings>;
15
26
  }
27
+ /**
28
+ * 新しい Slot インスタンスを作成します。
29
+ *
30
+ * @template TName スロットの名前となる文字列リテラル型です。
31
+ * @template TValue スロットに許容される値の型です。
32
+ * @param name スロット名です。
33
+ * @param defaultValue デフォルト値です。
34
+ * @returns 作成された新しい Slot インスタンスです。
35
+ */
36
+ declare function slot<const TName extends string, TValue extends RawValue = RawValue>(name: TName, defaultValue?: TValue): sql.Slot<TName, TValue>;
16
37
  /**
17
38
  * テンプレートリテラルを使用して SQL クエリーを安全に構築するためのタグ関数です。
18
39
  *
40
+ * @template TRawBindings クエリーに渡される生の値のタプル型です。
19
41
  * @param strings テンプレートリテラルの静的な文字列部分の配列です。
20
42
  * @param bindings テンプレートリテラルに埋め込まれた動的な値の配列です。
21
43
  * @returns パラメーター化された SQL 情報を保持する Sql インスタンスを返します。
@@ -24,15 +46,25 @@ declare namespace sql {
24
46
  * const query = sql`SELECT * FROM users WHERE id = ${1}`;
25
47
  * ```
26
48
  */
27
- declare const sql: ((strings: TemplateStringsArray, ...bindings: readonly sql.RawValue[]) => sql.Sql) & {
49
+ declare const sql: (<const TRawBindings extends readonly RawValue[]>(strings: TemplateStringsArray, ...bindings: TRawBindings) => sql.Sql<TRawBindings>) & {
28
50
  /**
29
- * Sql クラス自体への参照です。
51
+ * Sql クラスです。
30
52
  */
31
53
  readonly Sql: typeof Sql;
32
54
  /**
33
55
  * 生の文字列を SQL 断片として扱うための関数です。
34
56
  */
35
57
  readonly raw: typeof raw;
58
+ /**
59
+ * 新しい Slot インスタンスを作成する関数です。
60
+ */
61
+ readonly slot: typeof slot;
62
+ /**
63
+ * スロットを表すクラスです。
64
+ *
65
+ * スロットは後から値を注入可能なプレースホルダーです。
66
+ */
67
+ readonly Slot: typeof Slot;
36
68
  /**
37
69
  * 複数の SQL 断片を結合するための関数です。
38
70
  */
@@ -40,7 +72,7 @@ declare const sql: ((strings: TemplateStringsArray, ...bindings: readonly sql.Ra
40
72
  /**
41
73
  * 空の SQL クエリーを表す定数です。
42
74
  */
43
- readonly empty: Sql;
75
+ readonly empty: Sql<readonly unknown[]>;
44
76
  /**
45
77
  * 識別子(テーブル名等)を安全にエスケープするための関数です。
46
78
  */
@@ -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,OAAO,EAAE,MAAM,WAAW,CAAC;AAElE,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;;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,QAAQ,EAAE,MAAM,WAAW,CAAC;AAElF,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;;;;;;;OAOG;IACH,KAAY,IAAI,CACd,KAAK,SAAS,MAAM,GAAG,MAAM,EAC7B,MAAM,SAAS,QAAQ,GAAG,QAAQ,IAChC,OAAO,WAAW,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAE5C;;;;OAIG;IACH,KAAY,GAAG,CAAC,YAAY,SAAS,SAAS,QAAQ,EAAE,GAAG,SAAS,QAAQ,EAAE,IAC5E,OAAO,WAAW,EAAE,GAAG,CAAC,YAAY,CAAC,CAAC;CACzC;AAED;;;;;;;;GAQG;AACH,iBAAS,IAAI,CAAC,KAAK,CAAC,KAAK,SAAS,MAAM,EAAE,MAAM,SAAS,QAAQ,GAAG,QAAQ,EAC1E,IAAI,EAAE,KAAK,EACX,YAAY,CAAC,EAAE,MAAM,GACpB,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;AAM3B;;;;;;;;;;;GAWG;AACH,QAAA,MAAM,GAAG,UACY,YAAY,SAAS,SAAS,QAAQ,EAAE,WAChD,oBAAoB,eAChB,YAAY,KACxB,GAAG,CAAC,GAAG,CAAC,YAAY,CAAC;IAItB;;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,7 +1,11 @@
1
- import { empty, join, raw, Sql, ident, literal } 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
  *
8
+ * @template TRawBindings クエリーに渡される生の値のタプル型です。
5
9
  * @param strings テンプレートリテラルの静的な文字列部分の配列です。
6
10
  * @param bindings テンプレートリテラルに埋め込まれた動的な値の配列です。
7
11
  * @returns パラメーター化された SQL 情報を保持する Sql インスタンスを返します。
@@ -14,13 +18,23 @@ const sql = /*#__PURE__*/ Object.assign(function sql(strings, ...bindings) {
14
18
  return new Sql(strings, bindings);
15
19
  }, {
16
20
  /**
17
- * Sql クラス自体への参照です。
21
+ * Sql クラスです。
18
22
  */
19
23
  Sql,
20
24
  /**
21
25
  * 生の文字列を SQL 断片として扱うための関数です。
22
26
  */
23
27
  raw,
28
+ /**
29
+ * 新しい Slot インスタンスを作成する関数です。
30
+ */
31
+ slot,
32
+ /**
33
+ * スロットを表すクラスです。
34
+ *
35
+ * スロットは後から値を注入可能なプレースホルダーです。
36
+ */
37
+ Slot,
24
38
  /**
25
39
  * 複数の SQL 断片を結合するための関数です。
26
40
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgsql-template-tag",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "",
5
5
  "homepage": "https://github.com/tai-kun/pgsql-template-tag",
6
6
  "license": "MIT",
@@ -25,6 +25,10 @@
25
25
  "default": "./dist/src/index.js"
26
26
  }
27
27
  },
28
+ "dependencies": {
29
+ "es-toolkit": "^1.46.1",
30
+ "type-fest": "^5.6.0"
31
+ },
28
32
  "devDependencies": {
29
33
  "@tsconfig/node24": "^24.0.4",
30
34
  "@tsconfig/strictest": "^2.0.8",
@@ -33,9 +37,9 @@
33
37
  "@vitest/browser": "^4.1.5",
34
38
  "@vitest/browser-playwright": "^4.1.5",
35
39
  "npm-check-updates": "^20.0.2",
36
- "oxfmt": "^0.46.0",
37
- "oxlint": "^1.61.0",
38
- "oxlint-tsgolint": "^0.22.0",
40
+ "oxfmt": "^0.47.0",
41
+ "oxlint": "^1.62.0",
42
+ "oxlint-tsgolint": "^0.22.1",
39
43
  "playwright": "^1.59.1",
40
44
  "typescript": "^6.0.3",
41
45
  "vite": "^8.0.10",
package/src/core.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import { isPlainObject } from "es-toolkit/predicate";
2
+ import type { UnionToTuple } from "type-fest";
3
+
1
4
  /**
2
5
  * SQL クエリー内で使用される値の型定義です。
3
6
  */
@@ -7,7 +10,167 @@ export type Value = unknown;
7
10
  * SQL クエリーの構築に使用できる生の値、または別の Sql インスタンスを表す型です。
8
11
  */
9
12
  // oxlint-disable-next-line typescript/no-redundant-type-constituents
10
- export type RawValue = Value | Sql;
13
+ export type RawValue = Value | Slot | Sql;
14
+
15
+ /**
16
+ * Slot クラスを一意に識別するためのシンボルです。
17
+ */
18
+ declare const SLOT_SYMBOL: unique symbol;
19
+
20
+ /**
21
+ * Slot クラスの基底となる型定義です。
22
+ */
23
+ const SlotTypes = class {} as {
24
+ new (): {
25
+ /**
26
+ * このプロパティーは、TypeScript の `extends Slot` で `Slot` インスタンスのみに一致させるためにあります。そのため、`Slot` と同じプロパティーを持つオブジェクトに対して一致することはありません。
27
+ */
28
+ readonly ["~kind"]: typeof SLOT_SYMBOL;
29
+ };
30
+ };
31
+
32
+ /**
33
+ * スロットを表すクラスです。
34
+ *
35
+ * スロットは後から値を注入可能なプレースホルダーです。
36
+ *
37
+ * @template TName スロットの名前となる文字列リテラル型です。
38
+ * @template TValue スロットに許容される値の型です。
39
+ */
40
+ export class Slot<
41
+ const TName extends string = string,
42
+ TValue extends RawValue = RawValue,
43
+ > extends SlotTypes {
44
+ /**
45
+ * スロット名です。
46
+ */
47
+ public readonly name: TName;
48
+
49
+ /**
50
+ * デフォルト値です。
51
+ */
52
+ public readonly defaultValue: TValue;
53
+
54
+ /**
55
+ * 新しい Slot インスタンスを初期化します。
56
+ *
57
+ * @param name スロット名です。
58
+ * @param defaultValue デフォルト値です。
59
+ */
60
+ public constructor(
61
+ ...args: null extends TValue
62
+ ? [name: TName, defaultValue?: TValue]
63
+ : [name: TName, defaultValue: TValue]
64
+ );
65
+
66
+ public constructor(name: TName, defaultValue?: RawValue) {
67
+ super();
68
+
69
+ if (arguments.length < 2) {
70
+ defaultValue = null;
71
+ }
72
+
73
+ this.name = name;
74
+ this.defaultValue = defaultValue as TValue;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * オブジェクトのバリューの型を抽出するヘルパー型です。
80
+ *
81
+ * @template T 対象となるオブジェクト型です。
82
+ */
83
+ type $ValueOf<T> = T[keyof T];
84
+
85
+ /**
86
+ * スロットの配列から、再帰的に値をマージして型を決定します。
87
+ *
88
+ * @template TSlots スロットのタプル型です。
89
+ */
90
+ type $MergeSlotValue<TSlots> = TSlots extends [
91
+ Slot<string, infer TValue>,
92
+ infer TSlot,
93
+ ...infer TOtherSlots,
94
+ ]
95
+ ? TValue & $MergeSlotValue<[TSlot, ...TOtherSlots]>
96
+ : TSlots extends [Slot<string, infer TValue>]
97
+ ? TValue
98
+ : never;
99
+
100
+ /**
101
+ * RawValue の配列からスロット情報を抽出し、名前ごとのマップ型に変換します。
102
+ *
103
+ * @template TValues RawValue の読み取り専用配列型です。
104
+ */
105
+ type $MapSlotValue<TValues extends readonly RawValue[]> =
106
+ TValues extends readonly (infer TSlot extends Slot)[]
107
+ ? {
108
+ // `Slot<"id", string | number> | Slot<"id", string>` の場合、`(string | number) & (string)` となるように、各スロットの積集合をとります。
109
+ [TName in TSlot["name"]]: $MergeSlotValue<UnionToTuple<Extract<TSlot, Slot<TName>>>>;
110
+ }
111
+ : {};
112
+
113
+ /**
114
+ * 指定された値のリストに対して、スロットを実際の内容で置き換えた型を生成します。
115
+ *
116
+ * @template TValues 置き換え対象の配列型です。
117
+ * @template TSlots スロット名と値のマップ型です。
118
+ */
119
+ type $FillSlots<TValues, TSlots> = TValues extends [infer TValue, ...infer TOtherValues]
120
+ ? [
121
+ TValue extends Slot<infer TName extends Extract<keyof TSlots, string>, infer TSlotValue>
122
+ ? TSlotValue extends TSlots[TName]
123
+ ? TSlotValue
124
+ : TValue
125
+ : TValue,
126
+ ...$FillSlots<TOtherValues, TSlots>,
127
+ ]
128
+ : [];
129
+
130
+ /**
131
+ * スロットを埋めるための部分的な引数型を定義します。
132
+ *
133
+ * @template TSlots スロット名と値のマップ型です。
134
+ */
135
+ type _FillSlots<TSlots> =
136
+ | {
137
+ readonly [TName in Extract<keyof TSlots, string>]?: TSlots[TName];
138
+ }
139
+ | Iterable<
140
+ readonly [
141
+ name: $ValueOf<{
142
+ [TName in Extract<keyof TSlots, string>]: TName | Slot<TName, TSlots[TName]>;
143
+ }>,
144
+ value: $ValueOf<TSlots>,
145
+ ]
146
+ >;
147
+
148
+ /**
149
+ * すべてのスロットを埋めるために必要な引数型を定義します。
150
+ *
151
+ * @template TSlots スロット名と値のマップ型です。
152
+ */
153
+ type _FillAllSlots<TSlots> = {
154
+ readonly [TName in Extract<keyof TSlots, string>]: TSlots[TName];
155
+ };
156
+
157
+ /**
158
+ * スロットの部分的な補完に使用する外部向けの型定義です。
159
+ *
160
+ * @template TValues RawValue の配列です。
161
+ */
162
+ export type FillSlots<TValues extends readonly RawValue[] = readonly RawValue[]> = _FillSlots<
163
+ $MapSlotValue<TValues>
164
+ >;
165
+
166
+ /**
167
+ * すべてのスロットの強制的な補完に使用する外部向けの型定義です。
168
+ *
169
+ * @template TValues RawValue の配列です。
170
+ */
171
+ export type FillAllSlots<TValues extends readonly RawValue[] = readonly RawValue[]> = _FillAllSlots<
172
+ $MapSlotValue<TValues>
173
+ >;
11
174
 
12
175
  /**
13
176
  * Sql クラスの内部状態を管理するためのプライベートな型定義です。
@@ -16,24 +179,37 @@ type PrivateState = {
16
179
  /**
17
180
  * クエリーを構成する静的な文字列の配列です。
18
181
  */
19
- readonly s: readonly [string, ...string[]];
182
+ readonly parts: readonly [string, ...string[]];
20
183
 
21
184
  /**
22
185
  * プレースホルダーに対応するインデックス(1 から始まる数値)の配列です。
23
186
  */
24
- readonly p: readonly number[];
187
+ readonly phIds: readonly number[];
188
+
189
+ /**
190
+ * values のインデックス -> スロット情報 のマップです。
191
+ */
192
+ readonly idx2slot: ReadonlyMap<number, Slot>;
193
+
194
+ /**
195
+ * スロット名 -> values のインデックス のマップです。
196
+ */
197
+ readonly slot2idx: ReadonlyMap<string, ReadonlySet<number>>;
25
198
 
26
199
  /**
27
200
  * キャッシュされた最終的なクエリーテキストです。
28
201
  */
29
- t?: string;
202
+ text?: string;
30
203
  };
31
204
 
32
205
  /**
33
206
  * 安全な SQL クエリーを構築するためのクラスです。
207
+ *
34
208
  * プレースホルダーを使用したパラメーター化クエリーを生成します。
209
+ *
210
+ * @template TRawBindings クエリーに渡される生の値のタプル型です。
35
211
  */
36
- export class Sql {
212
+ export class Sql<const TRawBindings extends readonly RawValue[] = readonly RawValue[]> {
37
213
  /**
38
214
  * 構築された SQL クエリーテキストを取得します。
39
215
  *
@@ -43,19 +219,19 @@ export class Sql {
43
219
  */
44
220
  public get text(): string {
45
221
  // キャッシュが存在しない場合にのみ、文字列を構築します。
46
- if (this._.t === undefined) {
222
+ if (this.#state.text === undefined) {
47
223
  let i = 0,
48
- text = this._.s[0];
224
+ text = this.#state.parts[0];
49
225
 
50
226
  // 文字列の断片とプレースホルダー($1, $2...)を交互に結合します。
51
- for (; i < this._.p.length; i++) {
52
- text += "$" + this._.p[i] + this._.s[i + 1];
227
+ for (; i < this.#state.phIds.length; i++) {
228
+ text += "$" + this.#state.phIds[i] + this.#state.parts[i + 1];
53
229
  }
54
230
 
55
- this._.t = text;
231
+ this.#state.text = text;
56
232
  }
57
233
 
58
- return this._.t;
234
+ return this.#state.text;
59
235
  }
60
236
 
61
237
  /**
@@ -66,7 +242,7 @@ export class Sql {
66
242
  /**
67
243
  * 内部状態を保持するためのプロパティーです。
68
244
  */
69
- private readonly _!: PrivateState;
245
+ readonly #state: PrivateState;
70
246
 
71
247
  /**
72
248
  * 新しい Sql インスタンスを初期化します。
@@ -74,7 +250,7 @@ export class Sql {
74
250
  * @param rawStrings SQL の断片となる文字列の配列です。
75
251
  * @param rawBindings 文字列の間に挿入される値の配列です。
76
252
  */
77
- public constructor(rawStrings: readonly string[], rawBindings: readonly RawValue[]) {
253
+ public constructor(rawStrings: readonly string[], rawBindings: TRawBindings) {
78
254
  if (rawStrings.length === 0) {
79
255
  throw new TypeError("Expected at least 1 string");
80
256
  }
@@ -88,10 +264,52 @@ export class Sql {
88
264
  const strings: [string, ...string[]] = [rawStrings[0]!];
89
265
  const bindings: Value[] = [];
90
266
  const placeholderIds: number[] = [];
267
+ const idx2slot = new Map<number, Slot>();
268
+ const slot2idx = new Map<string, Set<number>>();
91
269
 
92
270
  /** 値の重複を排除し、同じ値には同じプレースホルダー ID を割り当てるためのマップです。 */
93
271
  const valueToId = new Map<Value, number>();
94
272
 
273
+ /** スロットごとのプレースホルダー ID を管理します。 */
274
+ const slotToId = new Map<Slot, number>();
275
+
276
+ /**
277
+ * 値を bindings に登録し、placeholderId を取得します。
278
+ */
279
+ const registerValue = (value: Value): number => {
280
+ let placeholderId = valueToId.get(value);
281
+ if (placeholderId === undefined) {
282
+ bindings.push(value);
283
+ placeholderId = bindings.length;
284
+ valueToId.set(value, placeholderId);
285
+ }
286
+
287
+ return placeholderId;
288
+ };
289
+
290
+ /**
291
+ * スロットを bindings に登録し、placeholderId を取得します。
292
+ */
293
+ const registerSlot = (slot: Slot): number => {
294
+ let placeholderId = slotToId.get(slot);
295
+ if (placeholderId === undefined) {
296
+ bindings.push(slot.defaultValue);
297
+ placeholderId = bindings.length;
298
+ slotToId.set(slot, placeholderId);
299
+
300
+ const index = placeholderId - 1;
301
+ idx2slot.set(index, slot);
302
+ let idxes = slot2idx.get(slot.name);
303
+ if (idxes === undefined) {
304
+ idxes = new Set();
305
+ slot2idx.set(slot.name, idxes);
306
+ }
307
+ idxes.add(index);
308
+ }
309
+
310
+ return placeholderId;
311
+ };
312
+
95
313
  // 提供された全てのバインディング値を走査して、SQL 文字列と値を正規化します。
96
314
  for (let i = 0; i < rawBindings.length; i++) {
97
315
  const child = rawBindings[i];
@@ -100,33 +318,26 @@ export class Sql {
100
318
  // バインディング値が Sql インスタンス(ネストされたクエリー)の場合の処理です。
101
319
  if (child instanceof Sql) {
102
320
  // 現在の最後の文字列断片に、ネストされた Sql の最初の断片を結合します。
103
- strings[strings.length - 1] += child._.s[0];
321
+ strings[strings.length - 1] += child.#state.parts[0];
104
322
 
105
323
  // ネストされた 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
- }
324
+ for (let j = 0; j < child.#state.phIds.length; j++) {
325
+ const childPlaceholderId = child.#state.phIds[j]!;
326
+ const valueIndex = childPlaceholderId - 1;
327
+ const value = child.values[valueIndex]!;
328
+
329
+ const slot = child.#state.idx2slot.get(valueIndex);
330
+
331
+ const placeholderId = slot !== undefined ? registerSlot(slot) : registerValue(value);
116
332
 
117
- strings.push(child._.s[j + 1]!);
333
+ strings.push(child.#state.parts[j + 1]!);
118
334
  placeholderIds.push(placeholderId);
119
335
  }
120
336
 
121
337
  // ネストされた Sql の展開が終わった後に、後続の生の文字列を結合します。
122
338
  strings[strings.length - 1] += rawString;
123
339
  } 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
- }
340
+ const placeholderId = child instanceof Slot ? registerSlot(child) : registerValue(child);
130
341
 
131
342
  strings.push(rawString);
132
343
  placeholderIds.push(placeholderId);
@@ -135,13 +346,102 @@ export class Sql {
135
346
 
136
347
  this.values = bindings;
137
348
 
138
- // 内部状態を隠蔽し、不必要なプロパティーの露出を防ぎます。
139
- Object.defineProperty(this, "_", {
140
- value: {
141
- s: strings,
142
- p: placeholderIds,
143
- },
144
- });
349
+ this.#state = {
350
+ parts: strings,
351
+ phIds: placeholderIds,
352
+ idx2slot,
353
+ slot2idx,
354
+ };
355
+ }
356
+
357
+ /**
358
+ * スロットを値で埋める内部メソッドです。
359
+ *
360
+ * @param slots スロットのマップまたはエントリーの配列です。
361
+ * @param all 全てのスロットが埋まっているかチェックするかどうかです。
362
+ * @returns 新しい Sql インスタンスを返します。
363
+ */
364
+ #fill(slots: FillSlots<Slot[]>, all: boolean): Sql {
365
+ if (isPlainObject(slots)) {
366
+ slots = Object.entries(slots);
367
+ } else {
368
+ // 一旦 Map のインスタンスにすることで、重複する名前またはスロットを 1 つに絞ります。
369
+ slots = new Map(slots);
370
+ }
371
+
372
+ const { parts, idx2slot, slot2idx } = this.#state;
373
+ const filled = new Set<number>();
374
+ const values = this.values.slice();
375
+ for (const [target, value] of new Map(slots)) {
376
+ let idxes: ReadonlySet<number> | undefined;
377
+ if (typeof target === "string") {
378
+ idxes = slot2idx.get(target);
379
+ } else {
380
+ // インスタンスが直接指定された場合、全インデックスから一致するものを探します。
381
+ for (const [idx, slot] of idx2slot) {
382
+ if (slot === target) {
383
+ idxes ||= new Set();
384
+ (idxes as Set<number>).add(idx);
385
+ }
386
+ }
387
+ }
388
+
389
+ if (idxes === undefined) {
390
+ // スロットが見つからない場合は無視します。
391
+ continue;
392
+ }
393
+
394
+ // 該当するすべてのプレースホルダーインデックスを新しい値で更新します。
395
+ for (const idx of idxes) {
396
+ values[idx] = value;
397
+ filled.add(idx);
398
+ }
399
+ }
400
+
401
+ // 全て埋める必要がある場合、未解決のスロットが残っていないか検証します。
402
+ if (all) {
403
+ for (let idx = 0; idx < values.length; idx++) {
404
+ if (idx2slot.has(idx) && !filled.has(idx)) {
405
+ const missingSlots = new Set<string>();
406
+ missingSlots.add(idx2slot.get(idx)!.name);
407
+ for (; idx < values.length; idx++) {
408
+ if (idx2slot.has(idx)) {
409
+ missingSlots.add(idx2slot.get(idx)!.name);
410
+ }
411
+ }
412
+
413
+ throw new Error(`Not all slots are filled. Missing: ${[...missingSlots].join(", ")}`);
414
+ }
415
+ }
416
+ }
417
+
418
+ return new Sql(parts, values);
419
+ }
420
+
421
+ /**
422
+ * スロットを値で埋めます。
423
+ *
424
+ * @template TSlots 指定されたスロットのマップ型です。
425
+ * @param slots スロットの値です。
426
+ * @returns スロットが埋められた新しい Sql インスタンスです。
427
+ */
428
+ public fill<TSlots extends FillSlots<TRawBindings>>(
429
+ slots: TSlots,
430
+ ): Sql<$FillSlots<TRawBindings, TSlots>> {
431
+ return this.#fill(slots, false);
432
+ }
433
+
434
+ /**
435
+ * すべてのスロットを値で埋めます。
436
+ *
437
+ * @template TSlots 全てのスロットをカバーするマップ型です。
438
+ * @param slots スロットの値です。
439
+ * @returns スロットが埋められた新しい Sql インスタンスです。
440
+ */
441
+ public fillAll<TSlots extends FillAllSlots<TRawBindings>>(
442
+ slots: TSlots,
443
+ ): Sql<$FillSlots<TRawBindings, TSlots>> {
444
+ return this.#fill(slots, true);
145
445
  }
146
446
 
147
447
  /**
@@ -171,6 +471,7 @@ export class Sql {
171
471
 
172
472
  /**
173
473
  * 生の文字列を SQL 断片として扱います。
474
+ *
174
475
  * この値はパラメーター化の対象にならず、そのままクエリーに含まれます。
175
476
  *
176
477
  * @param value SQL に含める生の文字列です。
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, literal } from "./core.js";
1
+ import { empty, join, raw, Sql, ident, literal, Slot, RawValue } from "./core.js";
2
2
 
3
3
  namespace sql {
4
4
  /**
@@ -11,15 +11,50 @@ namespace sql {
11
11
  */
12
12
  export type Value = import("./core.js").Value;
13
13
 
14
+ /**
15
+ * スロットを表すクラスです。
16
+ *
17
+ * スロットは後から値を注入可能なプレースホルダーです。
18
+ *
19
+ * @template TName スロットの名前となる文字列リテラル型です。
20
+ * @template TValue スロットに許容される値の型です。
21
+ */
22
+ export type Slot<
23
+ TName extends string = string,
24
+ TValue extends RawValue = RawValue,
25
+ > = import("./core.js").Slot<TName, TValue>;
26
+
14
27
  /**
15
28
  * 安全な SQL クエリーを構築するためのクラス型です。
29
+ *
30
+ * @template TRawBindings クエリーに渡される生の値のタプル型です。
16
31
  */
17
- export type Sql = import("./core.js").Sql;
32
+ export type Sql<TRawBindings extends readonly RawValue[] = readonly RawValue[]> =
33
+ import("./core.js").Sql<TRawBindings>;
34
+ }
35
+
36
+ /**
37
+ * 新しい Slot インスタンスを作成します。
38
+ *
39
+ * @template TName スロットの名前となる文字列リテラル型です。
40
+ * @template TValue スロットに許容される値の型です。
41
+ * @param name スロット名です。
42
+ * @param defaultValue デフォルト値です。
43
+ * @returns 作成された新しい Slot インスタンスです。
44
+ */
45
+ function slot<const TName extends string, TValue extends RawValue = RawValue>(
46
+ name: TName,
47
+ defaultValue?: TValue,
48
+ ): sql.Slot<TName, TValue>;
49
+
50
+ function slot(...args: [any]): sql.Slot {
51
+ return new sql.Slot(...args);
18
52
  }
19
53
 
20
54
  /**
21
55
  * テンプレートリテラルを使用して SQL クエリーを安全に構築するためのタグ関数です。
22
56
  *
57
+ * @template TRawBindings クエリーに渡される生の値のタプル型です。
23
58
  * @param strings テンプレートリテラルの静的な文字列部分の配列です。
24
59
  * @param bindings テンプレートリテラルに埋め込まれた動的な値の配列です。
25
60
  * @returns パラメーター化された SQL 情報を保持する Sql インスタンスを返します。
@@ -29,12 +64,15 @@ namespace sql {
29
64
  * ```
30
65
  */
31
66
  const sql = /*#__PURE__*/ Object.assign(
32
- function sql(strings: TemplateStringsArray, ...bindings: readonly sql.RawValue[]): sql.Sql {
67
+ function sql<const TRawBindings extends readonly RawValue[]>(
68
+ strings: TemplateStringsArray,
69
+ ...bindings: TRawBindings
70
+ ): sql.Sql<TRawBindings> {
33
71
  return new Sql(strings, bindings);
34
72
  },
35
73
  {
36
74
  /**
37
- * Sql クラス自体への参照です。
75
+ * Sql クラスです。
38
76
  */
39
77
  Sql,
40
78
 
@@ -43,6 +81,18 @@ const sql = /*#__PURE__*/ Object.assign(
43
81
  */
44
82
  raw,
45
83
 
84
+ /**
85
+ * 新しい Slot インスタンスを作成する関数です。
86
+ */
87
+ slot,
88
+
89
+ /**
90
+ * スロットを表すクラスです。
91
+ *
92
+ * スロットは後から値を注入可能なプレースホルダーです。
93
+ */
94
+ Slot,
95
+
46
96
  /**
47
97
  * 複数の SQL 断片を結合するための関数です。
48
98
  */