You have a really broad question, but hopefully this can get you started. I would begin with some basic wrappers around the semantic components of your SQL query
const Field = (name) =>
({ type: Field, name })
const SqlLiteral = (value) =>
({ type: SqlLiteral, value })
const Condition = (operator, left, right) =>
({ type: Condition, operator, left, right })
Then you make an expression->sql expander
const toSql = (expr) =>
{
switch (expr.type) {
case Field:
return expr.name
case SqlLiteral:
return JSON.stringify (expr.value) // just works for strings, numbers
case Condition:
return toSql (expr.left) + expr.operator + toSql (expr.right)
default:
throw Error (`Unhandled expression type: ${expr.type}`)
}
}
Test some expressions
toSql (Condition ("=", Field ("name"), SqlLiteral ("bruce")))
// name="bruce"
toSql (Condition (">", Field ("earnings"), Field ("expenses")))
// earnings>expenses
toSql (Condition (">", Field ("earnings"), SqlLiteral (100)))
// earnings>100
We can just keep adding to this
const And = (left, right) =>
({ type: And, left, right })
const toSql = (expr) =>
{
switch (expr.type) {
case And:
return toSql (expr.left) + " AND " + toSql (expr.right)
...
}
}
toSql
( And ( Condition ("=", Field ("first"), SqlLiteral ("bruce"))
, Condition ("=", Field ("last"), SqlLiteral ("lee"))
)
)
// first="bruce" AND last="lee"
Keep going... We can support things like SQL function calls like this
const SqlFunc = (func, arg) =>
({ type: SqlFunc, func, arg })
const toSql = (expr) =>
{
switch (expr.type) {
case SqlFunc:
return expr.func + "(" + toSql (expr.arg) + ")"
...
}
}
toSql
( Condition ( "<"
, SqlFunc ("strlen", Field ("name"))
, SqlLiteral (10)
)
)
// strlen(name)<10
Keep going!
const Select = (from, ...fields) =>
({ type: Select, from, fields: fields.map(Field) })
const Table = (name) =>
({ type: Field, name })
const Where = (select, condition) =>
({ type: Where, select, condition })
const toSql = (expr) =>
{
switch (expr.type) {
case Select:
return `SELECT ${expr.fields.map(toSql).join(',')} FROM ${toSql (expr.from)}`
case Field:
case Table:
return expr.name
case Where:
return toSql (expr.select) + " WHERE " + toSql (expr.condition)
...
}
}
Now let's see a more advanced query come to life
toSql
( Where ( Select ( Table ("people")
, "first"
, "last"
, "email"
, "age"
)
, And ( Condition ("=", Field ("first"), SqlLiteral ("bruce"))
, Condition ("=", Field ("last"), SqlLiteral ("lee"))
)
)
)
// SELECT first,last,email,age FROM people WHERE first="bruce" AND last="lee"
Obviously there's tons of work to do here, but the idea is once you have all the building blocks and a suitable toSql expander, you can make the magic wrappers around it all.
For example
const where = (descriptor = {}) =>
Object.entries (descriptor)
.map (([ k, v ]) =>
Condition ("=", Field (k), SqlLiteral (v)))
.reduce (And)
toSql (where ({ first: "bruce", last: "lee"}))
// first="bruce" AND last="lee"'
My general advice to you would be not to start this from scratch unless you want to build it for the sole purpose of learning how to make it. SQL is vastly complex and there are countless other projects out there that have attempted something similar in various ways. Look to them to see how to handle the more tricky scenarios.
Full program demonstration below
const Select = (from, ...fields) =>
({ type: Select, from, fields: fields.map(Field) })
const Table = (name) =>
({ type: Field, name })
const Field = (name) =>
({ type: Field, name })
const SqlLiteral = (value) =>
({ type: SqlLiteral, value })
const Condition = (operator, left, right) =>
({ type: Condition, operator, left, right })
const And = (left, right, ...more) =>
more.length === 0
? ({ type: And, left, right })
: And (left, And (right, ...more))
const Where = (select, condition) =>
({ type: Where, select, condition })
const SqlFunc = (func, arg) =>
({ type: SqlFunc, func, arg })
const toSql = (expr) =>
{
switch (expr.type) {
case Select:
return `SELECT ${expr.fields.map(toSql).join(',')} FROM ${toSql (expr.from)}`
case Field:
return expr.name
case Table:
return expr.name
case SqlLiteral:
return JSON.stringify (expr.value) // just works for strings, numbers
case SqlFunc:
return expr.func + "(" + toSql (expr.arg) + ")"
case Condition:
return toSql (expr.left) + expr.operator + toSql (expr.right)
case And:
return toSql (expr.left) + " AND " + toSql (expr.right)
case Where:
return toSql (expr.select) + " WHERE " + toSql (expr.condition)
default:
throw Error (`Unhandled expression type: ${JSON.stringify(expr)}`)
}
}
const sql =
toSql(
Where ( Select ( Table ("people")
, "first"
, "last"
, "email"
, "age"
)
, And ( Condition ("=", Field ("first"), SqlLiteral ("bruce"))
, Condition ("=", Field ("last"), SqlLiteral ("lee"))
, Condition ( ">"
, Field ("age")
, SqlLiteral (30)
)
)
)
)
console.log (sql)
// SELECT first,last,email,age FROM people WHERE first="bruce" AND last="lee" AND age>30