Skip to content

Commit b9106aa

Browse files
author
Marcus Pousette
committed
node adapter: analyze SQL placeholders; use varargs for positional and bind-first for named; add param count slicing to avoid CI binding quirks
1 parent ca6aaf4 commit b9106aa

File tree

1 file changed

+46
-9
lines changed

1 file changed

+46
-9
lines changed

src/unified-node.ts

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -115,21 +115,57 @@ export async function createDatabase(
115115
};
116116

117117
type Method = 'run' | 'get' | 'all';
118+
type ParamStyle = 'positional' | 'named' | 'none';
119+
type PositionalKind = 'numeric' | 'anonymous' | undefined;
120+
interface SqlMeta {
121+
style: ParamStyle;
122+
positionalKind?: PositionalKind;
123+
paramCount: number;
124+
}
125+
126+
const analyzeSql = (sql: string): SqlMeta => {
127+
// Very lightweight analysis; sufficient for our test/usage.
128+
const namedRe = /[:@$][A-Za-z_][A-Za-z0-9_]*/g;
129+
const numericRe = /\?(\d+)/g;
130+
const anonRe = /\?(?!\d)/g;
131+
const hasNamed = namedRe.test(sql);
132+
if (hasNamed) return { style: 'named', paramCount: 0 };
133+
let maxIndex = 0;
134+
let m: RegExpExecArray | null;
135+
numericRe.lastIndex = 0;
136+
while ((m = numericRe.exec(sql))) {
137+
const idx = parseInt(m[1]!, 10);
138+
if (idx > maxIndex) maxIndex = idx;
139+
}
140+
if (maxIndex > 0) return { style: 'positional', positionalKind: 'numeric', paramCount: maxIndex };
141+
// Count anonymous ? occurrences
142+
const qs = sql.match(anonRe)?.length ?? 0;
143+
if (qs > 0) return { style: 'positional', positionalKind: 'anonymous', paramCount: qs };
144+
return { style: 'none', paramCount: 0 };
145+
};
146+
118147
const mapParams = (v: any): any | undefined => {
119148
if (v == null) return undefined;
120149
if (Array.isArray(v)) return toNumericParamObj(normVals(v) ?? []);
121150
return normObj(v);
122151
};
123-
const callWithParams = (stmt: any, method: Method, v: any) => {
152+
const callWithParams = (stmt: any, method: Method, v: any, meta: SqlMeta) => {
124153
const params = mapParams(v);
125-
if (DEBUG) dlog(`stmt.${method}()`, { params: summarize(params) });
154+
if (DEBUG) dlog(`stmt.${method}()`, { params: summarize(params), meta });
155+
// Prefer varargs for positional placeholders to avoid CI differences.
156+
if (Array.isArray(v) && meta.style === 'positional') {
157+
const a = normVals(v) ?? [];
158+
const toUse = meta.paramCount > 0 && a.length > meta.paramCount ? a.slice(0, meta.paramCount) : a;
159+
if (a.length > toUse.length && DEBUG) dlog('slicing extra params for positional binding', { have: a.length, use: toUse.length });
160+
return stmt[method](...toUse);
161+
}
162+
// Fallback: deterministic bind-first with (possibly named) object.
126163
if (params === undefined) return stmt[method]();
127-
// Deterministic: bind first, then call without inline params.
128164
stmt.bind(params);
129165
return stmt[method]();
130166
};
131167

132-
const wrapStmt = (stmt: any): Statement => {
168+
const wrapStmt = (stmt: any, meta: SqlMeta): Statement => {
133169
let bound: any[] | undefined = undefined;
134170
return {
135171
bind(values: any[]) {
@@ -139,19 +175,19 @@ export async function createDatabase(
139175
finalize() {},
140176
get(values?: any[]) {
141177
const v = (values ?? bound) as any;
142-
return callWithParams(stmt, 'get', v);
178+
return callWithParams(stmt, 'get', v, meta);
143179
},
144180
run(values: any[]) {
145181
const v = (values ?? bound) as any;
146-
callWithParams(stmt, 'run', v);
182+
callWithParams(stmt, 'run', v, meta);
147183
bound = undefined;
148184
},
149185
async reset() {
150186
return this;
151187
},
152188
all(values: any[]) {
153189
const v = (values ?? bound) as any;
154-
return callWithParams(stmt, 'all', v);
190+
return callWithParams(stmt, 'all', v, meta);
155191
},
156192
step() {
157193
return false;
@@ -175,8 +211,9 @@ export async function createDatabase(
175211
await (prev.reset?.() as any);
176212
return prev;
177213
}
178-
const stmt = open().prepare(sql);
179-
const wrapped = wrapStmt(stmt);
214+
const meta = analyzeSql(sql);
215+
const stmt = open().prepare(sql);
216+
const wrapped = wrapStmt(stmt, meta);
180217
if (id != null) statements.set(id, wrapped);
181218
return wrapped;
182219
},

0 commit comments

Comments
 (0)