You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

@filtron/sql

Package Overview
Dependencies
Maintainers
1
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@filtron/sql - npm Package Compare versions

Comparing version
1.0.0
to
1.1.0
+10
dist/index.js.map
{
"version": 3,
"sources": ["../src/converter.ts"],
"sourcesContent": [
"/**\n * SQL converter for Filtron AST\n * Converts Filtron AST nodes to parameterized SQL WHERE clauses\n */\n\nimport type {\n\tASTNode,\n\tValue,\n\tComparisonOperator,\n\tOrExpression,\n\tAndExpression,\n\tNotExpression,\n\tComparisonExpression,\n\tOneOfExpression,\n\tNotOneOfExpression,\n\tExistsExpression,\n\tBooleanFieldExpression,\n\tRangeExpression,\n} from \"@filtron/core\";\n\n/**\n * SQL generation result\n */\nexport interface SQLResult {\n\t/** SQL WHERE clause (without the WHERE keyword) */\n\tsql: string;\n\t/** Array of parameter values in order */\n\tparams: unknown[];\n}\n\n/**\n * SQL generation options\n */\nexport interface SQLOptions {\n\t/**\n\t * Parameter placeholder style\n\t * - 'numbered': PostgreSQL/DuckDB style ($1, $2, $3)\n\t * - 'question': MySQL/SQLite/DuckDB style (?, ?, ?)\n\t * @default 'numbered'\n\t */\n\tparameterStyle?: \"numbered\" | \"question\";\n\n\t/**\n\t * Custom field name mapper\n\t * Useful for escaping field names or mapping to table columns\n\t * @default (field) => field\n\t */\n\tfieldMapper?: (field: string) => string;\n\n\t/**\n\t * Value mapper for LIKE operator (~)\n\t * Allows adding wildcards or escaping special characters\n\t * Applied to the value before parameterization\n\t * @default (value) => value\n\t *\n\t * @example\n\t * ```typescript\n\t * // Auto-wrap LIKE values with wildcards for \"contains\" search\n\t * toSQL(ast, {\n\t * valueMapper: (value) => `%${escapeLike(String(value))}%`\n\t * })\n\t * ```\n\t */\n\tvalueMapper?: (value: string | number | boolean) => string | number | boolean;\n\n\t/**\n\t * Starting parameter index (for numbered parameters)\n\t * @default 1\n\t */\n\tstartIndex?: number;\n}\n\n/**\n * Internal state for SQL generation\n */\ninterface GeneratorState {\n\tparams: unknown[];\n\tparameterStyle: \"numbered\" | \"question\";\n\tfieldMapper: (field: string) => string;\n\tvalueMapper: (value: string | number | boolean) => string | number | boolean;\n\tparamIndex: number;\n}\n\n/**\n * Converts a Filtron AST to a parameterized SQL WHERE clause\n *\n * @param ast - The Filtron AST node to convert\n * @param options - SQL generation options\n * @returns SQL result with WHERE clause and parameters\n *\n * @example\n * ```typescript\n * import { parse } from 'filtron';\n * import { toSQL } from '@filtron/sql';\n *\n * const ast = parse('age > 18 AND status = \"active\"');\n * if (ast.success) {\n * const { sql, params } = toSQL(ast.ast);\n * console.log(sql); // \"age > $1 AND status = $2\"\n * console.log(params); // [18, \"active\"]\n * }\n * ```\n */\nexport function toSQL(ast: ASTNode, options: SQLOptions = {}): SQLResult {\n\tconst state: GeneratorState = {\n\t\tparams: [],\n\t\tparameterStyle: options.parameterStyle ?? \"numbered\",\n\t\tfieldMapper: options.fieldMapper ?? ((field) => field),\n\t\tvalueMapper: options.valueMapper ?? ((value) => value),\n\t\tparamIndex: options.startIndex ?? 1,\n\t};\n\n\tconst sql = generateSQL(ast, state);\n\n\treturn {\n\t\tsql,\n\t\tparams: state.params,\n\t};\n}\n\n/**\n * Recursively generates SQL from AST nodes\n */\nfunction generateSQL(node: ASTNode, state: GeneratorState): string {\n\tswitch (node.type) {\n\t\tcase \"or\":\n\t\t\treturn generateOr(node, state);\n\t\tcase \"and\":\n\t\t\treturn generateAnd(node, state);\n\t\tcase \"not\":\n\t\t\treturn generateNot(node, state);\n\t\tcase \"comparison\":\n\t\t\treturn generateComparison(node, state);\n\t\tcase \"oneOf\":\n\t\t\treturn generateOneOf(node, state);\n\t\tcase \"notOneOf\":\n\t\t\treturn generateNotOneOf(node, state);\n\t\tcase \"exists\":\n\t\t\treturn generateExists(node, state);\n\t\tcase \"booleanField\":\n\t\t\treturn generateBooleanField(node, state);\n\t\tcase \"range\":\n\t\t\treturn generateRange(node, state);\n\t\tdefault:\n\t\t\t// TypeScript exhaustiveness check\n\t\t\tconst _exhaustive: never = node;\n\t\t\tthrow new Error(`Unknown node type: ${(node as ASTNode).type}`);\n\t}\n}\n\n/**\n * Generates SQL for OR expression\n */\nfunction generateOr(node: OrExpression, state: GeneratorState): string {\n\tconst left = generateSQL(node.left, state);\n\tconst right = generateSQL(node.right, state);\n\treturn `(${left} OR ${right})`;\n}\n\n/**\n * Generates SQL for AND expression\n */\nfunction generateAnd(node: AndExpression, state: GeneratorState): string {\n\tconst left = generateSQL(node.left, state);\n\tconst right = generateSQL(node.right, state);\n\treturn `(${left} AND ${right})`;\n}\n\n/**\n * Generates SQL for NOT expression\n */\nfunction generateNot(node: NotExpression, state: GeneratorState): string {\n\tconst expr = generateSQL(node.expression, state);\n\treturn `NOT (${expr})`;\n}\n\n/**\n * Generates SQL for comparison expression\n */\nfunction generateComparison(node: ComparisonExpression, state: GeneratorState): string {\n\tconst field = state.fieldMapper(node.field);\n\tconst operator = mapComparisonOperator(node.operator);\n\n\t// Apply valueMapper for LIKE operator\n\tlet value = extractValue(node.value);\n\tif (node.operator === \"~\") {\n\t\tvalue = state.valueMapper(value);\n\t}\n\n\tconst param = addParameter(value, state);\n\n\treturn `${field} ${operator} ${param}`;\n}\n\n/**\n * Generates SQL for one-of expression (IN clause)\n */\nfunction generateOneOf(node: OneOfExpression, state: GeneratorState): string {\n\tconst field = state.fieldMapper(node.field);\n\tconst values = node.values.map((v) => extractValue(v));\n\n\tif (values.length === 0) {\n\t\t// Empty IN clause - always false\n\t\treturn \"1 = 0\";\n\t}\n\n\tconst placeholders = values.map((value) => addParameter(value, state));\n\treturn `${field} IN (${placeholders.join(\", \")})`;\n}\n\n/**\n * Generates SQL for not-one-of expression (NOT IN clause)\n */\nfunction generateNotOneOf(node: NotOneOfExpression, state: GeneratorState): string {\n\tconst field = state.fieldMapper(node.field);\n\tconst values = node.values.map((v) => extractValue(v));\n\n\tif (values.length === 0) {\n\t\t// Empty NOT IN clause - always true\n\t\treturn \"1 = 1\";\n\t}\n\n\tconst placeholders = values.map((value) => addParameter(value, state));\n\treturn `${field} NOT IN (${placeholders.join(\", \")})`;\n}\n\n/**\n * Generates SQL for exists expression\n */\nfunction generateExists(node: ExistsExpression, state: GeneratorState): string {\n\tconst field = state.fieldMapper(node.field);\n\treturn `${field} IS NOT NULL`;\n}\n\n/**\n * Generates SQL for boolean field expression\n */\nfunction generateBooleanField(node: BooleanFieldExpression, state: GeneratorState): string {\n\tconst field = state.fieldMapper(node.field);\n\tconst param = addParameter(true, state);\n\treturn `${field} = ${param}`;\n}\n\n/**\n * Generates SQL for range expression (BETWEEN)\n */\nfunction generateRange(node: RangeExpression, state: GeneratorState): string {\n\tconst field = state.fieldMapper(node.field);\n\tconst minParam = addParameter(node.min, state);\n\tconst maxParam = addParameter(node.max, state);\n\treturn `${field} BETWEEN ${minParam} AND ${maxParam}`;\n}\n\n/**\n * Maps Filtron comparison operators to SQL operators\n */\nfunction mapComparisonOperator(operator: ComparisonOperator): string {\n\tswitch (operator) {\n\t\tcase \"=\":\n\t\tcase \":\":\n\t\t\treturn \"=\";\n\t\tcase \"!=\":\n\t\t\treturn \"!=\";\n\t\tcase \"~\":\n\t\t\treturn \"LIKE\";\n\t\tcase \">\":\n\t\t\treturn \">\";\n\t\tcase \">=\":\n\t\t\treturn \">=\";\n\t\tcase \"<\":\n\t\t\treturn \"<\";\n\t\tcase \"<=\":\n\t\t\treturn \"<=\";\n\t\tdefault:\n\t\t\tconst _exhaustive: never = operator;\n\t\t\tthrow new Error(`Unknown operator: ${operator as string}`);\n\t}\n}\n\n/**\n * Extracts the primitive value from a Filtron Value node\n */\nfunction extractValue(value: Value): string | number | boolean {\n\tswitch (value.type) {\n\t\tcase \"string\":\n\t\t\treturn value.value;\n\t\tcase \"number\":\n\t\t\treturn value.value;\n\t\tcase \"boolean\":\n\t\t\treturn value.value;\n\t\tcase \"identifier\":\n\t\t\t// Identifiers are treated as strings in SQL context\n\t\t\treturn value.value;\n\t\tdefault:\n\t\t\tconst _exhaustive: never = value;\n\t\t\tthrow new Error(`Unknown value type: ${(value as Value).type}`);\n\t}\n}\n\n/**\n * Adds a parameter to the state and returns the placeholder\n */\nfunction addParameter(value: unknown, state: GeneratorState): string {\n\tstate.params.push(value);\n\n\tif (state.parameterStyle === \"numbered\") {\n\t\tconst placeholder = `$${state.paramIndex}`;\n\t\tstate.paramIndex++;\n\t\treturn placeholder;\n\t}\n\n\treturn \"?\";\n}\n\n/**\n * Escapes special LIKE characters (%, _, \\) in a string value\n * Use this to prevent LIKE injection when user input is used in LIKE patterns\n *\n * @param value - The value to escape\n * @returns Escaped string safe for use in LIKE patterns\n *\n * @example\n * ```typescript\n * escapeLike(\"admin%\") // \"admin\\\\%\"\n * escapeLike(\"test_user\") // \"test\\\\_user\"\n * ```\n */\nexport function escapeLike(value: string): string {\n\treturn value\n\t\t.replace(/\\\\/g, \"\\\\\\\\\") // Escape backslashes first\n\t\t.replace(/%/g, \"\\\\%\") // Escape % wildcard\n\t\t.replace(/_/g, \"\\\\_\"); // Escape _ single-char wildcard\n}\n\n/**\n * Wraps a value with wildcards for \"contains\" matching\n * Automatically escapes special LIKE characters\n *\n * @param value - The value to wrap\n * @returns Value wrapped with % wildcards\n *\n * @example\n * ```typescript\n * toSQL(ast, { valueMapper: contains })\n * // \"foo\" becomes \"%foo%\"\n * ```\n */\nexport function contains(value: string | number | boolean): string {\n\treturn `%${escapeLike(String(value))}%`;\n}\n\n/**\n * Adds trailing wildcard for \"starts with\" matching\n * Automatically escapes special LIKE characters\n *\n * @param value - The value to wrap\n * @returns Value with % wildcard at the end\n *\n * @example\n * ```typescript\n * toSQL(ast, { valueMapper: prefix })\n * // \"admin\" becomes \"admin%\"\n * ```\n */\nexport function prefix(value: string | number | boolean): string {\n\treturn `${escapeLike(String(value))}%`;\n}\n\n/**\n * Adds leading wildcard for \"ends with\" matching\n * Automatically escapes special LIKE characters\n *\n * @param value - The value to wrap\n * @returns Value with % wildcard at the beginning\n *\n * @example\n * ```typescript\n * toSQL(ast, { valueMapper: suffix })\n * // \".pdf\" becomes \"%.pdf\"\n * ```\n */\nexport function suffix(value: string | number | boolean): string {\n\treturn `%${escapeLike(String(value))}`;\n}\n"
],
"mappings": "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",
"debugId": "7C5E6C130D5FBAFC64756E2164756E21",
"names": []
}
+1
-1

@@ -23,3 +23,3 @@ /**

*/
export { toSQL } from "./src/converter.js";
export { toSQL, escapeLike, contains, prefix, suffix } from "./src/converter.js";
export type { SQLResult, SQLOptions } from "./src/converter.js";

@@ -1,1 +0,4 @@

function L(c,E={}){let q={params:[],parameterStyle:E.parameterStyle??"numbered",fieldMapper:E.fieldMapper??((S)=>S),paramIndex:E.startIndex??1};return{sql:b(c,q),params:q.params}}function b(c,E){switch(c.type){case"or":return T(c,E);case"and":return W(c,E);case"not":return $(c,E);case"comparison":return i(c,E);case"oneOf":return j(c,E);case"notOneOf":return m(c,E);case"exists":return A(c,E);case"booleanField":return D(c,E);default:let q=c;throw Error(`Unknown node type: ${c.type}`)}}function T(c,E){let q=b(c.left,E),R=b(c.right,E);return`(${q} OR ${R})`}function W(c,E){let q=b(c.left,E),R=b(c.right,E);return`(${q} AND ${R})`}function $(c,E){return`NOT (${b(c.expression,E)})`}function i(c,E){let q=E.fieldMapper(c.field),R=F(c.operator),S=w(H(c.value),E);return`${q} ${R} ${S}`}function j(c,E){let q=E.fieldMapper(c.field),R=c.values.map((y)=>H(y));if(R.length===0)return"1 = 0";let S=R.map((y)=>w(y,E));return`${q} IN (${S.join(", ")})`}function m(c,E){let q=E.fieldMapper(c.field),R=c.values.map((y)=>H(y));if(R.length===0)return"1 = 1";let S=R.map((y)=>w(y,E));return`${q} NOT IN (${S.join(", ")})`}function A(c,E){return`${E.fieldMapper(c.field)} IS NOT NULL`}function D(c,E){let q=E.fieldMapper(c.field),R=w(!0,E);return`${q} = ${R}`}function F(c){switch(c){case"=":case":":return"=";case"!=":return"!=";case"~":return"LIKE";case">":return">";case">=":return">=";case"<":return"<";case"<=":return"<=";default:let E=c;throw Error(`Unknown operator: ${c}`)}}function H(c){switch(c.type){case"string":return c.value;case"number":return c.value;case"boolean":return c.value;case"identifier":return c.value;default:let E=c;throw Error(`Unknown value type: ${c.type}`)}}function w(c,E){if(E.params.push(c),E.parameterStyle==="numbered"){let q=`$${E.paramIndex}`;return E.paramIndex++,q}return"?"}export{L as toSQL};
function F(q,E={}){let R={params:[],parameterStyle:E.parameterStyle??"numbered",fieldMapper:E.fieldMapper??((H)=>H),valueMapper:E.valueMapper??((H)=>H),paramIndex:E.startIndex??1};return{sql:$(q,R),params:R.params}}function $(q,E){switch(q.type){case"or":return T(q,E);case"and":return b(q,E);case"not":return z(q,E);case"comparison":return A(q,E);case"oneOf":return Q(q,E);case"notOneOf":return U(q,E);case"exists":return C(q,E);case"booleanField":return G(q,E);case"range":return J(q,E);default:let R=q;throw Error(`Unknown node type: ${q.type}`)}}function T(q,E){let R=$(q.left,E),S=$(q.right,E);return`(${R} OR ${S})`}function b(q,E){let R=$(q.left,E),S=$(q.right,E);return`(${R} AND ${S})`}function z(q,E){return`NOT (${$(q.expression,E)})`}function A(q,E){let R=E.fieldMapper(q.field),S=M(q.operator),H=D(q.value);if(q.operator==="~")H=E.valueMapper(H);let W=j(H,E);return`${R} ${S} ${W}`}function Q(q,E){let R=E.fieldMapper(q.field),S=q.values.map((W)=>D(W));if(S.length===0)return"1 = 0";let H=S.map((W)=>j(W,E));return`${R} IN (${H.join(", ")})`}function U(q,E){let R=E.fieldMapper(q.field),S=q.values.map((W)=>D(W));if(S.length===0)return"1 = 1";let H=S.map((W)=>j(W,E));return`${R} NOT IN (${H.join(", ")})`}function C(q,E){return`${E.fieldMapper(q.field)} IS NOT NULL`}function G(q,E){let R=E.fieldMapper(q.field),S=j(!0,E);return`${R} = ${S}`}function J(q,E){let R=E.fieldMapper(q.field),S=j(q.min,E),H=j(q.max,E);return`${R} BETWEEN ${S} AND ${H}`}function M(q){switch(q){case"=":case":":return"=";case"!=":return"!=";case"~":return"LIKE";case">":return">";case">=":return">=";case"<":return"<";case"<=":return"<=";default:let E=q;throw Error(`Unknown operator: ${q}`)}}function D(q){switch(q.type){case"string":return q.value;case"number":return q.value;case"boolean":return q.value;case"identifier":return q.value;default:let E=q;throw Error(`Unknown value type: ${q.type}`)}}function j(q,E){if(E.params.push(q),E.parameterStyle==="numbered"){let R=`$${E.paramIndex}`;return E.paramIndex++,R}return"?"}function y(q){return q.replace(/\\/g,"\\\\").replace(/%/g,"\\%").replace(/_/g,"\\_")}function N(q){return`%${y(String(q))}%`}function X(q){return`${y(String(q))}%`}function Y(q){return`%${y(String(q))}`}export{F as toSQL,Y as suffix,X as prefix,y as escapeLike,N as contains};
//# debugId=7C5E6C130D5FBAFC64756E2164756E21
//# sourceMappingURL=index.js.map

@@ -33,2 +33,17 @@ /**

/**
* Value mapper for LIKE operator (~)
* Allows adding wildcards or escaping special characters
* Applied to the value before parameterization
* @default (value) => value
*
* @example
* ```typescript
* // Auto-wrap LIKE values with wildcards for "contains" search
* toSQL(ast, {
* valueMapper: (value) => `%${escapeLike(String(value))}%`
* })
* ```
*/
valueMapper?: (value: string | number | boolean) => string | number | boolean;
/**
* Starting parameter index (for numbered parameters)

@@ -60,1 +75,57 @@ * @default 1

export declare function toSQL(ast: ASTNode, options?: SQLOptions): SQLResult;
/**
* Escapes special LIKE characters (%, _, \) in a string value
* Use this to prevent LIKE injection when user input is used in LIKE patterns
*
* @param value - The value to escape
* @returns Escaped string safe for use in LIKE patterns
*
* @example
* ```typescript
* escapeLike("admin%") // "admin\\%"
* escapeLike("test_user") // "test\\_user"
* ```
*/
export declare function escapeLike(value: string): string;
/**
* Wraps a value with wildcards for "contains" matching
* Automatically escapes special LIKE characters
*
* @param value - The value to wrap
* @returns Value wrapped with % wildcards
*
* @example
* ```typescript
* toSQL(ast, { valueMapper: contains })
* // "foo" becomes "%foo%"
* ```
*/
export declare function contains(value: string | number | boolean): string;
/**
* Adds trailing wildcard for "starts with" matching
* Automatically escapes special LIKE characters
*
* @param value - The value to wrap
* @returns Value with % wildcard at the end
*
* @example
* ```typescript
* toSQL(ast, { valueMapper: prefix })
* // "admin" becomes "admin%"
* ```
*/
export declare function prefix(value: string | number | boolean): string;
/**
* Adds leading wildcard for "ends with" matching
* Automatically escapes special LIKE characters
*
* @param value - The value to wrap
* @returns Value with % wildcard at the beginning
*
* @example
* ```typescript
* toSQL(ast, { valueMapper: suffix })
* // ".pdf" becomes "%.pdf"
* ```
*/
export declare function suffix(value: string | number | boolean): string;
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@filtron/sql",
"version": "1.0.0",
"version": "1.1.0",
"description": "SQL WHERE clause generator for Filtron AST with parameterized queries",

@@ -47,10 +47,12 @@ "keywords": [

"bench": "bun --expose-gc run benchmark.ts",
"build": "rm -rf dist && bun build --minify --splitting --outdir=dist --external @filtron/core index.ts && tsc -p tsconfig.build.json",
"lint": "oxlint --type-aware && oxfmt",
"build": "bun build --minify --splitting --sourcemap=linked --outdir=dist --external @filtron/core index.ts && tsc",
"prepublishOnly": "bun run build && bun test",
"test": "bun test"
"test": "bun test",
"typecheck": "tsc --noemit"
},
"dependencies": {
"@filtron/core": "1.0.0"
},
"peerDependencies": {
"@filtron/core": "1.0.0",
"typescript": ">=5.0.0"
"typescript": "5.9.3"
},

@@ -63,4 +65,3 @@ "peerDependenciesMeta": {

"devDependencies": {
"@filtron/core": "1.0.0",
"@types/bun": "1.3.2",
"@types/bun": "1.3.3",
"mitata": "1.0.34",

@@ -73,3 +74,3 @@ "typescript": "5.9.3"

},
"packageManager": "bun@1.3.2"
"packageManager": "bun@1.3.3"
}
+22
-358

@@ -5,18 +5,6 @@ # @filtron/sql

Convert Filtron query language AST to safe, parameterized SQL WHERE clauses that prevent SQL injection.
## Features
- **Safe**: Parameterized queries prevent SQL injection
- **Flexible**: Support for PostgreSQL/DuckDB ($1), MySQL/SQLite/DuckDB (?) parameter styles
- **Type-safe**: Full TypeScript support
- **Customizable**: Map field names to table columns
- **Zero dependencies**: Only requires `filtron` peer dependency
## Installation
```bash
bun add @filtron/core @filtron/sql
# or
npm install @filtron/core @filtron/sql
bun add @filtron/sql
```

@@ -30,13 +18,9 @@

// Parse a query
const result = parse('age > 18 AND status = "active"');
if (result.success) {
// Convert to SQL
const { sql, params } = toSQL(result.ast);
// sql: "(age > $1 AND status = $2)"
// params: [18, "active"]
console.log(sql); // "(age > $1 AND status = $2)"
console.log(params); // [18, "active"]
// Use with your database
const users = await db.query(`SELECT * FROM users WHERE ${sql}`, params);

@@ -50,363 +34,43 @@ }

Converts a Filtron AST node to a parameterized SQL WHERE clause.
Converts a Filtron AST to a parameterized SQL WHERE clause.
**Parameters:**
**Options:**
- `ast` (ASTNode): The Filtron AST to convert
- `options` (SQLOptions, optional): Generation options
| Option | Type | Description |
|--------|------|-------------|
| `parameterStyle` | `'numbered'` \| `'question'` | Placeholder style: `$1` (default) or `?` |
| `fieldMapper` | `(field) => string` | Transform field names to column names |
| `valueMapper` | `(value) => value` | Transform values (useful for LIKE wildcards) |
| `startIndex` | `number` | Starting parameter index (default: `1`) |
**Returns:** `SQLResult`
- `sql` (string): SQL WHERE clause (without the WHERE keyword)
- `params` (unknown[]): Array of parameter values in order
## Parameter Styles
### PostgreSQL/DuckDB Style (Default)
Uses numbered placeholders: `$1`, `$2`, `$3`, etc.
```typescript
const { sql, params } = toSQL(ast);
// sql: "age > $1 AND status = $2"
// params: [18, "active"]
// PostgreSQL
await client.query(`SELECT * FROM users WHERE ${sql}`, params);
// DuckDB
await db.all(`SELECT * FROM users WHERE ${sql}`, ...params);
```
### MySQL/SQLite/DuckDB Style
Uses question mark placeholders: `?`, `?`, `?`, etc.
```typescript
const { sql, params } = toSQL(ast, {
parameterStyle: "question",
});
// sql: "age > ? AND status = ?"
// params: [18, "active"]
// MySQL
await connection.query(`SELECT * FROM users WHERE ${sql}`, params);
// SQLite
db.all(`SELECT * FROM users WHERE ${sql}`, params, callback);
// DuckDB (also supports this style)
await db.all(`SELECT * FROM users WHERE ${sql}`, ...params);
```
## Field Mapping
Map Filtron field names to database column names:
```typescript
const { sql, params } = toSQL(ast, {
parameterStyle: "question", // for MySQL/SQLite
fieldMapper: (field) => `users.${field}`,
});
// Input: age > 18
// Output: users.age > $1
```
### Escaping Field Names
### Helper Functions
```typescript
const { sql, params } = toSQL(ast, {
fieldMapper: (field) => `"${field}"`,
});
// Input: user-name = "john"
// Output: "user-name" = $1
```
Helpers for use with `valueMapper` when using the LIKE operator (`~`):
### Table Aliases
```typescript
const { sql, params } = toSQL(ast, {
fieldMapper: (field) => `u.${field}`,
});
// Input: status = "active" AND verified
// Output: (u.status = $1 AND u.verified = $2)
```
import { toSQL, contains, prefix, suffix, escapeLike } from "@filtron/sql";
## Custom Start Index
// contains("foo") → "%foo%"
// prefix("foo") → "foo%"
// suffix("foo") → "%foo"
// escapeLike("foo%bar") → "foo\\%bar"
Useful when combining multiple queries:
```typescript
// First query uses $1, $2
const query1 = toSQL(ast1);
// Second query starts at $3
const query2 = toSQL(ast2, {
startIndex: query1.params.length + 1,
});
const sql = `${query1.sql} OR ${query2.sql}`;
const params = [...query1.params, ...query2.params];
const { sql, params } = toSQL(ast, { valueMapper: contains });
```
## Operator Mapping
| Filtron Operator | SQL Operator |
| ---------------- | ------------ |
| `=`, `:` | `=` |
| `!=` | `!=` |
| `>` | `>` |
| `>=` | `>=` |
| `<` | `<` |
| `<=` | `<=` |
| `~` | `LIKE` |
## Expression Types
### Comparison
```typescript
// Filtron: age > 18
// SQL: age > $1
// Params: [18]
```
### Boolean Logic
```typescript
// Filtron: age > 18 AND status = "active"
// SQL: (age > $1 AND status = $2)
// Params: [18, "active"]
// Filtron: role = "admin" OR role = "mod"
// SQL: (role = $1 OR role = $2)
// Params: ["admin", "mod"]
// Filtron: NOT suspended
// SQL: NOT (suspended = $1)
// Params: [true]
```
### One-of (IN clause)
```typescript
// Filtron: status : ["pending", "approved", "active"]
// SQL: status IN ($1, $2, $3)
// Params: ["pending", "approved", "active"]
// Filtron: role !: ["guest", "banned"]
// SQL: role NOT IN ($1, $2)
// Params: ["guest", "banned"]
```
### Field Exists
```typescript
// Filtron: email?
// SQL: email IS NOT NULL
// Params: []
```
### Boolean Field
```typescript
// Filtron: verified
// SQL: verified = $1
// Params: [true]
```
## Real-world Examples
### User Filtering
```typescript
import { parseOrThrow } from "@filtron/core";
import { toSQL } from "@filtron/sql";
// Parse user input
const query = 'age >= 18 AND verified AND role : ["user", "premium"]';
const ast = parseOrThrow(query);
// Generate SQL
const { sql, params } = toSQL(ast, {
fieldMapper: (field) => `users.${field}`,
});
// Execute query
const users = await db.query(`SELECT * FROM users WHERE ${sql}`, params);
```
### Product Search
```typescript
const query =
'price <= 100 AND name ~ "laptop" AND category : ["electronics", "computers"]';
const ast = parseOrThrow(query);
const { sql, params } = toSQL(ast, {
parameterStyle: "question",
fieldMapper: (field) => `products.${field}`,
});
const products = await db.all(
`SELECT * FROM products WHERE ${sql} ORDER BY price`,
params,
);
```
### Access Control
```typescript
const query = '(role = "admin" OR role = "moderator") AND NOT suspended';
const ast = parseOrThrow(query);
const { sql, params } = toSQL(ast);
const hasAccess = await db.get(
`SELECT COUNT(*) as count FROM users WHERE id = ? AND ${sql}`,
[userId, ...params],
);
return hasAccess.count > 0;
```
### DuckDB - Analytical Queries
```typescript
import { parseOrThrow } from "@filtron/core";
import { toSQL } from "@filtron/sql";
// Query Parquet files directly
const query = 'region : ["US", "EU"] AND revenue > 10000';
const ast = parseOrThrow(query);
const { sql, params } = toSQL(ast, {
parameterStyle: "numbered", // DuckDB supports both $1 and ?
});
// DuckDB excels at analytical queries
const results = await db.all(
`
SELECT
region,
SUM(revenue) as total_revenue,
COUNT(*) as transaction_count
FROM read_parquet('sales_*.parquet')
WHERE ${sql}
GROUP BY region
ORDER BY total_revenue DESC
`,
...params,
);
```
### DuckDB - Time Series Filtering
```typescript
const query = 'sensor_type = "temperature" AND value > 25';
const ast = parseOrThrow(query);
const { sql, params } = toSQL(ast, {
parameterStyle: "numbered",
fieldMapper: (field) => `"${field}"`, // Quote field names
});
// Time-series aggregation
const timeSeries = await db.all(
`
SELECT
time_bucket('1 hour', timestamp) as hour,
AVG(value) as avg_value,
MAX(value) as max_value
FROM sensor_data
WHERE ${sql}
GROUP BY hour
ORDER BY hour
`,
...params,
);
```
## Performance
**SQL conversion adds minimal overhead to query parsing.**
### Running Benchmarks
Comprehensive benchmark suite with detailed metrics:
```bash
bun run bench
```
Quick summary showing overhead analysis:
```bash
bun run overhead.ts
```
### Key Metrics
- **SQL Conversion Overhead**: ~0.1-0.2μs per query
- **Impact**: Less than 1% of parse time
- **Throughput**: 11,000+ queries/sec (parse + SQL generation)
- **Memory**: Minimal allocations, efficient GC pressure
### Benchmark Results
```
Simple queries: 26-33 μs (parse + SQL)
Complex queries: 60-130 μs (parse + SQL)
SQL conversion only: 30-200 ns (isolated)
```
The overhead of SQL conversion is negligible compared to parsing, making it suitable for real-time API usage where every request may involve parsing and converting user filters.
## Security
**This library generates parameterized queries to prevent SQL injection.**
This library generates parameterized queries to prevent SQL injection:
✅ **Safe** - Values are passed as parameters:
```typescript
const { sql, params } = toSQL(ast);
db.query(`SELECT * FROM users WHERE ${sql}`, params);
// SQL: SELECT * FROM users WHERE name = $1
// Params: ["admin' OR '1'='1"]
// Params: ["admin' OR '1'='1"] - safely escaped
```
❌ **Unsafe** - Never concatenate values directly:
```typescript
// DON'T DO THIS!
const unsafeSQL = `SELECT * FROM users WHERE name = '${userInput}'`;
```
**Note:** Field names from the `fieldMapper` are **not** parameterized. Ensure field names come from trusted sources or validate them before use.
## TypeScript
Full type definitions included:
```typescript
import type { SQLResult, SQLOptions } from "@filtron/sql";
const options: SQLOptions = {
parameterStyle: "numbered",
fieldMapper: (field) => `t.${field}`,
startIndex: 1,
};
const result: SQLResult = toSQL(ast, options);
// result.sql: string
// result.params: unknown[]
```
## License
MIT - See [LICENSE](../../LICENSE)
## Links
- **Filtron**: https://github.com/jbergstroem/filtron
- **GitHub**: https://github.com/jbergstroem/filtron/tree/main/packages/sql
- **npm**: https://www.npmjs.com/package/@filtron/sql