@happyvertical/sql 0.74.8 → 0.74.10
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.
- package/README.md +36 -0
- package/dist/aggregate.d.ts +34 -0
- package/dist/aggregate.d.ts.map +1 -0
- package/dist/chunks/{duckdb-Djx8WKZE.js → duckdb-CIywTPV3.js} +2 -2
- package/dist/chunks/{duckdb-Djx8WKZE.js.map → duckdb-CIywTPV3.js.map} +1 -1
- package/dist/chunks/{index-DL-5nB7l.js → index-DrPgGVZz.js} +244 -20
- package/dist/chunks/index-DrPgGVZz.js.map +1 -0
- package/dist/chunks/{sqlite-DkCGjXmu.js → sqlite-Cxjsirpn.js} +3 -3
- package/dist/chunks/{sqlite-DkCGjXmu.js.map → sqlite-Cxjsirpn.js.map} +1 -1
- package/dist/chunks/{sqlite-native-B6q-0nLs.js → sqlite-native-BdArMasZ.js} +2 -2
- package/dist/chunks/{sqlite-native-B6q-0nLs.js.map → sqlite-native-BdArMasZ.js.map} +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -13
- package/dist/shared/utils.d.ts +10 -0
- package/dist/shared/utils.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/chunks/index-DL-5nB7l.js.map +0 -1
|
@@ -22,18 +22,43 @@ const VALID_OPERATORS = {
|
|
|
22
22
|
"<=": "<=",
|
|
23
23
|
"!=": "!=",
|
|
24
24
|
like: "LIKE",
|
|
25
|
-
in: "IN"
|
|
25
|
+
in: "IN",
|
|
26
|
+
"not in": "NOT IN"
|
|
26
27
|
};
|
|
28
|
+
function isSimpleSqlIdentifier(field) {
|
|
29
|
+
return /^[a-zA-Z0-9_.]+$/.test(field);
|
|
30
|
+
}
|
|
31
|
+
function parseConditionKey(fullKey) {
|
|
32
|
+
const trimmed = fullKey.trim();
|
|
33
|
+
const lower = trimmed.toLowerCase();
|
|
34
|
+
const operators = ["not in", "like", "in", "!=", ">=", "<=", ">", "<", "="];
|
|
35
|
+
for (const operator of operators) {
|
|
36
|
+
if (lower.endsWith(` ${operator}`)) {
|
|
37
|
+
return {
|
|
38
|
+
field: trimmed.slice(0, -(operator.length + 1)).trim(),
|
|
39
|
+
operator,
|
|
40
|
+
explicitOperator: true
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { field: trimmed, operator: "=", explicitOperator: false };
|
|
45
|
+
}
|
|
27
46
|
const buildCondition = (fullKey, value, currIndex, adapterType) => {
|
|
28
|
-
const
|
|
47
|
+
const { field, operator, explicitOperator } = parseConditionKey(fullKey);
|
|
48
|
+
if (!explicitOperator && !isSimpleSqlIdentifier(field)) {
|
|
49
|
+
throw new Error(`Invalid SQL identifier: ${field}`);
|
|
50
|
+
}
|
|
29
51
|
const sqlOperator = VALID_OPERATORS[operator] || "=";
|
|
30
52
|
const values = [];
|
|
31
53
|
let sql;
|
|
32
54
|
if (value === null) {
|
|
33
55
|
sql = `${field} IS ${sqlOperator === "=" ? "NULL" : "NOT NULL"}`;
|
|
34
|
-
} else if (sqlOperator === "IN" && Array.isArray(value)) {
|
|
56
|
+
} else if ((sqlOperator === "IN" || sqlOperator === "NOT IN") && Array.isArray(value)) {
|
|
57
|
+
if (value.length === 0) {
|
|
58
|
+
throw new Error(`${sqlOperator} requires at least one value`);
|
|
59
|
+
}
|
|
35
60
|
const placeholders = value.map(() => `$${currIndex.value++}`).join(", ");
|
|
36
|
-
sql = `${field}
|
|
61
|
+
sql = `${field} ${sqlOperator} (${placeholders})`;
|
|
37
62
|
values.push(...value);
|
|
38
63
|
} else if (value instanceof Date) {
|
|
39
64
|
const needsCast = adapterType === "duckdb" || adapterType === "json";
|
|
@@ -92,6 +117,203 @@ const buildWhere = (where, startIndex = 1, adapterType) => {
|
|
|
92
117
|
}
|
|
93
118
|
return { sql, values };
|
|
94
119
|
};
|
|
120
|
+
const VALID_DIRECTIONS = /* @__PURE__ */ new Set(["ASC", "DESC"]);
|
|
121
|
+
const VALID_BUCKET_UNITS = /* @__PURE__ */ new Set([
|
|
122
|
+
"minute",
|
|
123
|
+
"hour",
|
|
124
|
+
"day",
|
|
125
|
+
"week",
|
|
126
|
+
"month",
|
|
127
|
+
"quarter",
|
|
128
|
+
"year"
|
|
129
|
+
]);
|
|
130
|
+
function validateSqlIdentifier(identifier) {
|
|
131
|
+
if (!/^[a-zA-Z0-9_.]+$/.test(identifier)) {
|
|
132
|
+
throw new Error(`Invalid SQL identifier: ${identifier}`);
|
|
133
|
+
}
|
|
134
|
+
return identifier;
|
|
135
|
+
}
|
|
136
|
+
function normalizeAdapter(adapterType) {
|
|
137
|
+
return adapterType ?? "sqlite";
|
|
138
|
+
}
|
|
139
|
+
function placeholder(index2) {
|
|
140
|
+
return `$${index2}`;
|
|
141
|
+
}
|
|
142
|
+
function bucketExpr(adapterType, unit, column) {
|
|
143
|
+
if (!VALID_BUCKET_UNITS.has(unit)) {
|
|
144
|
+
throw new Error(`Invalid aggregate bucket unit: ${unit}`);
|
|
145
|
+
}
|
|
146
|
+
const col = validateSqlIdentifier(column);
|
|
147
|
+
const adapter = normalizeAdapter(adapterType);
|
|
148
|
+
if (adapter === "postgres" || adapter === "duckdb" || adapter === "json") {
|
|
149
|
+
return `date_trunc('${unit}', ${col})`;
|
|
150
|
+
}
|
|
151
|
+
switch (unit) {
|
|
152
|
+
case "minute":
|
|
153
|
+
return `strftime('%Y-%m-%d %H:%M:00', ${col})`;
|
|
154
|
+
case "hour":
|
|
155
|
+
return `strftime('%Y-%m-%d %H:00:00', ${col})`;
|
|
156
|
+
case "day":
|
|
157
|
+
return `strftime('%Y-%m-%d 00:00:00', ${col})`;
|
|
158
|
+
case "week":
|
|
159
|
+
return `strftime('%Y-%m-%d 00:00:00', date(${col}, printf('-%d days', (CAST(strftime('%w', ${col}) AS INTEGER) + 6) % 7)))`;
|
|
160
|
+
case "month":
|
|
161
|
+
return `strftime('%Y-%m-01 00:00:00', ${col})`;
|
|
162
|
+
case "quarter":
|
|
163
|
+
return `strftime('%Y-%m-%d 00:00:00', date(strftime('%Y-', ${col}) || printf('%02d', ((CAST(strftime('%m', ${col}) AS INTEGER) - 1) / 3) * 3 + 1) || '-01'))`;
|
|
164
|
+
case "year":
|
|
165
|
+
return `strftime('%Y-01-01 00:00:00', ${col})`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function aggregateExpression(expr) {
|
|
169
|
+
const normalizedFn = String(expr.fn).toLowerCase();
|
|
170
|
+
const fn = validateSqlIdentifier(String(expr.fn)).toUpperCase();
|
|
171
|
+
if (normalizedFn === "count" && !expr.column) {
|
|
172
|
+
if (expr.distinct) {
|
|
173
|
+
throw new Error(`Aggregate ${expr.fn} with distinct requires a column`);
|
|
174
|
+
}
|
|
175
|
+
return "COUNT(*)";
|
|
176
|
+
}
|
|
177
|
+
if (!expr.column) {
|
|
178
|
+
throw new Error(`Aggregate ${expr.fn} requires a column`);
|
|
179
|
+
}
|
|
180
|
+
const col = validateSqlIdentifier(expr.column);
|
|
181
|
+
const distinct = expr.distinct ? "DISTINCT " : "";
|
|
182
|
+
return `${fn}(${distinct}${col})`;
|
|
183
|
+
}
|
|
184
|
+
function selectExpression(expr, adapterType) {
|
|
185
|
+
if ("column" in expr && !("fn" in expr) && !("bucket" in expr)) {
|
|
186
|
+
const column = validateSqlIdentifier(expr.column);
|
|
187
|
+
const alias2 = validateSqlIdentifier(expr.as ?? expr.column);
|
|
188
|
+
return { expression: column, alias: alias2, groupingCandidate: true };
|
|
189
|
+
}
|
|
190
|
+
if ("bucket" in expr) {
|
|
191
|
+
const expression2 = bucketExpr(adapterType, expr.bucket, expr.column);
|
|
192
|
+
const alias2 = validateSqlIdentifier(expr.as);
|
|
193
|
+
return { expression: expression2, alias: alias2, groupingCandidate: true };
|
|
194
|
+
}
|
|
195
|
+
const expression = aggregateExpression(expr);
|
|
196
|
+
const alias = validateSqlIdentifier(expr.as);
|
|
197
|
+
return { expression, alias, groupingCandidate: false };
|
|
198
|
+
}
|
|
199
|
+
function validateWhereShape(where) {
|
|
200
|
+
if (!where) return;
|
|
201
|
+
const validateKey = (key) => {
|
|
202
|
+
const { field } = parseConditionKey(key);
|
|
203
|
+
if (field) validateSqlIdentifier(field);
|
|
204
|
+
};
|
|
205
|
+
if (Array.isArray(where)) {
|
|
206
|
+
for (const andGroup of where) {
|
|
207
|
+
for (const condition of andGroup) {
|
|
208
|
+
for (const key of Object.keys(condition)) validateKey(key);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
for (const key of Object.keys(where)) validateKey(key);
|
|
214
|
+
}
|
|
215
|
+
function expandHavingKey(key, expressionByAlias) {
|
|
216
|
+
const parsed = parseConditionKey(key);
|
|
217
|
+
const expression = expressionByAlias.get(parsed.field) ?? validateSqlIdentifier(parsed.field);
|
|
218
|
+
return `${expression} ${parsed.operator}`;
|
|
219
|
+
}
|
|
220
|
+
function expandHaving(having, expressionByAlias) {
|
|
221
|
+
const expandCondition = (condition) => Object.fromEntries(
|
|
222
|
+
Object.entries(condition).map(([key, value]) => [
|
|
223
|
+
expandHavingKey(key, expressionByAlias),
|
|
224
|
+
value
|
|
225
|
+
])
|
|
226
|
+
);
|
|
227
|
+
if (Array.isArray(having)) {
|
|
228
|
+
return having.map(
|
|
229
|
+
(andGroup) => andGroup.map(
|
|
230
|
+
(condition) => expandCondition(condition)
|
|
231
|
+
)
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
return expandCondition(having);
|
|
235
|
+
}
|
|
236
|
+
function buildHaving(having, expressionByAlias, startIndex, adapterType) {
|
|
237
|
+
if (!having) return { sql: "", values: [] };
|
|
238
|
+
const expanded = expandHaving(having, expressionByAlias);
|
|
239
|
+
const built = buildWhere(expanded, startIndex, adapterType);
|
|
240
|
+
return {
|
|
241
|
+
sql: built.sql ? built.sql.replace(/^WHERE/, "HAVING") : "",
|
|
242
|
+
values: built.values
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function buildOrderBy(orderBy) {
|
|
246
|
+
if (!orderBy) return "";
|
|
247
|
+
const items = Array.isArray(orderBy) ? orderBy : [orderBy];
|
|
248
|
+
const parts = items.map((item) => {
|
|
249
|
+
const [field, direction = "ASC"] = item.trim().split(/\s+/);
|
|
250
|
+
const normalizedDirection = direction.toUpperCase();
|
|
251
|
+
if (!VALID_DIRECTIONS.has(normalizedDirection)) {
|
|
252
|
+
throw new Error(`Invalid aggregate order direction: ${direction}`);
|
|
253
|
+
}
|
|
254
|
+
const orderExpr = validateSqlIdentifier(field);
|
|
255
|
+
return `${orderExpr} ${normalizedDirection}`;
|
|
256
|
+
});
|
|
257
|
+
return `ORDER BY ${parts.join(", ")}`;
|
|
258
|
+
}
|
|
259
|
+
function buildAggregate(spec, startIndex = 1, adapterType) {
|
|
260
|
+
const from = validateSqlIdentifier(spec.from);
|
|
261
|
+
if (spec.select.length === 0) {
|
|
262
|
+
throw new Error(
|
|
263
|
+
"AggregateSpec.select must contain at least one expression"
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
validateWhereShape(spec.where);
|
|
267
|
+
const expressionByAlias = /* @__PURE__ */ new Map();
|
|
268
|
+
const groupingAliases = [];
|
|
269
|
+
const selectSql = spec.select.map((expr) => {
|
|
270
|
+
const selected = selectExpression(expr, adapterType);
|
|
271
|
+
expressionByAlias.set(selected.alias, selected.expression);
|
|
272
|
+
if (selected.groupingCandidate) {
|
|
273
|
+
groupingAliases.push(selected.alias);
|
|
274
|
+
}
|
|
275
|
+
return `${selected.expression} AS ${selected.alias}`;
|
|
276
|
+
});
|
|
277
|
+
const where = spec.where ? buildWhere(spec.where, startIndex, adapterType) : { sql: "", values: [] };
|
|
278
|
+
let nextParamIndex = startIndex + where.values.length;
|
|
279
|
+
const groupByItems = spec.groupBy ?? groupingAliases;
|
|
280
|
+
const groupBySql = groupByItems.length > 0 ? `GROUP BY ${groupByItems.map(
|
|
281
|
+
(item) => expressionByAlias.has(item) ? expressionByAlias.get(item) : validateSqlIdentifier(item)
|
|
282
|
+
).join(", ")}` : "";
|
|
283
|
+
const having = buildHaving(
|
|
284
|
+
spec.having,
|
|
285
|
+
expressionByAlias,
|
|
286
|
+
nextParamIndex,
|
|
287
|
+
adapterType
|
|
288
|
+
);
|
|
289
|
+
nextParamIndex += having.values.length;
|
|
290
|
+
const orderBySql = buildOrderBy(spec.orderBy);
|
|
291
|
+
const pagingValues = [];
|
|
292
|
+
const pagingSql = [];
|
|
293
|
+
if (spec.limit !== void 0) {
|
|
294
|
+
pagingSql.push(`LIMIT ${placeholder(nextParamIndex++)}`);
|
|
295
|
+
pagingValues.push(spec.limit);
|
|
296
|
+
} else if (spec.offset !== void 0 && normalizeAdapter(adapterType) === "sqlite") {
|
|
297
|
+
pagingSql.push("LIMIT -1");
|
|
298
|
+
}
|
|
299
|
+
if (spec.offset !== void 0) {
|
|
300
|
+
pagingSql.push(`OFFSET ${placeholder(nextParamIndex++)}`);
|
|
301
|
+
pagingValues.push(spec.offset);
|
|
302
|
+
}
|
|
303
|
+
const sql = [
|
|
304
|
+
`SELECT ${selectSql.join(", ")}`,
|
|
305
|
+
`FROM ${from}`,
|
|
306
|
+
where.sql,
|
|
307
|
+
groupBySql,
|
|
308
|
+
having.sql,
|
|
309
|
+
orderBySql,
|
|
310
|
+
pagingSql.join(" ")
|
|
311
|
+
].filter(Boolean).join(" ");
|
|
312
|
+
return {
|
|
313
|
+
sql,
|
|
314
|
+
values: [...where.values, ...having.values, ...pagingValues]
|
|
315
|
+
};
|
|
316
|
+
}
|
|
95
317
|
function parseIndexStatement(indexSQL) {
|
|
96
318
|
const trimmed = indexSQL.trim();
|
|
97
319
|
const match = trimmed.match(
|
|
@@ -3385,11 +3607,11 @@ async function getDatabase(options = {}) {
|
|
|
3385
3607
|
return postgres$1.getDatabase(options);
|
|
3386
3608
|
}
|
|
3387
3609
|
if (options.type === "sqlite") {
|
|
3388
|
-
const sqlite = await import("./sqlite-
|
|
3610
|
+
const sqlite = await import("./sqlite-Cxjsirpn.js");
|
|
3389
3611
|
return sqlite.getDatabase(options);
|
|
3390
3612
|
}
|
|
3391
3613
|
if (options.type === "duckdb") {
|
|
3392
|
-
const duckdb = await import("./duckdb-
|
|
3614
|
+
const duckdb = await import("./duckdb-CIywTPV3.js");
|
|
3393
3615
|
return duckdb.getDatabase(options);
|
|
3394
3616
|
}
|
|
3395
3617
|
if (options.type === "json") {
|
|
@@ -3450,19 +3672,21 @@ export {
|
|
|
3450
3672
|
convertUniqueIndexesToInlineConstraints as e,
|
|
3451
3673
|
formatDbError as f,
|
|
3452
3674
|
generateCreateIndexStatement as g,
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3675
|
+
bucketExpr as h,
|
|
3676
|
+
buildAggregate as i,
|
|
3677
|
+
checkExpectedTables as j,
|
|
3678
|
+
checkRelationship as k,
|
|
3679
|
+
checkUniqueColumn as l,
|
|
3680
|
+
clearConnectionCache as m,
|
|
3681
|
+
clearPostgresConnectionCache as n,
|
|
3682
|
+
index as o,
|
|
3683
|
+
escapeSqlValue as p,
|
|
3684
|
+
getDatabase as q,
|
|
3463
3685
|
resolveSchemas as r,
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3686
|
+
runDoctor as s,
|
|
3687
|
+
syncSchema as t,
|
|
3688
|
+
tableExists as u,
|
|
3689
|
+
validateTableName as v,
|
|
3690
|
+
validateColumnName as w
|
|
3467
3691
|
};
|
|
3468
|
-
//# sourceMappingURL=index-
|
|
3692
|
+
//# sourceMappingURL=index-DrPgGVZz.js.map
|