@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.
@@ -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 [field, operator = "="] = fullKey.split(" ");
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} IN (${placeholders})`;
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-DkCGjXmu.js");
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-Djx8WKZE.js");
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
- checkExpectedTables as h,
3454
- checkRelationship as i,
3455
- checkUniqueColumn as j,
3456
- clearConnectionCache as k,
3457
- clearPostgresConnectionCache as l,
3458
- index as m,
3459
- escapeSqlValue as n,
3460
- getDatabase as o,
3461
- runDoctor as p,
3462
- validateColumnName as q,
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
- syncSchema as s,
3465
- tableExists as t,
3466
- validateTableName as v
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-DL-5nB7l.js.map
3692
+ //# sourceMappingURL=index-DrPgGVZz.js.map