GoJson2SQL is a library for composing SQL queries using JSON. A JSON file is transformed into an SQL string. This library facilitates the process of generating SQL statements by utilizing a structured JSON format, enhancing the readability and simplicity of SQL query construction.
Currently, it can only perform SELECT queries.
- Basic select query
- Basic selection field
- Join Table
- Conditional (WHERE Statement)
- HAVING
- SQL Function
- CASE, WHEN and THEN in the selection fields
- Subqueries
- Parsing Value to Parameters
- SQLi Prevention (Experimental)
- More Queries
- Validate SQL Syntax
- ?
go get github.com/bonkzero404/gojson2sql
package main
import (
"fmt"
"github.com/bonkzero404/gojson2sql"
)
func main() {
sqlJson := `
{
"table": "table_1",
"selectFields": [
"a",
"b"
],
"conditions": [
{
"datatype": "number",
"clause": "a",
"operator": "=",
"value": 1
}
],
"limit": 1
}
`
jql, err := gojson2sql.NewJson2Sql([]byte(sqlJson), &gojson2sql.Json2SqlConf{})
if err != nil {
panic(err)
}
sql, param, _ := jql.Generate()
fmt.Println("SQL:", sql)
fmt.Println("Param:", param)
}
Output:
SQL: SELECT a, b FROM table_1 WHERE a = ? LIMIT
Param: [1]
You can use this raw SQL with either the sql package or GORM. Here's an example with sql package:
jql, _ := gojson2sql.NewJson2Sql([]byte(sqlJson), &gojson2sql.Json2SqlConf{})
sql, param, _ := jql.Generate()
db.Query(sql, param)
[
{
"table": "table_1",
"selectFields": ["a", "b"],
"conditions": [
{
"datatype": "number",
"clause": "a",
"operator": "=",
"value": 1
}
],
"limit": 1
},
{
"table": "table_2",
"selectFields": ["a", "b"],
"conditions": [
{
"datatype": "number",
"clause": "a",
"operator": "=",
"value": 1
}
],
"limit": 1
}
]
If you are using UNION, you must set the withUnion parameter in Json2SqlConf. Here is an example:
jql, _ := gojson2sql.NewJson2Sql([]byte(sqlJson), &gojson2sql.Json2SqlConf{
withUnion: true,
})
sql, param, _ := jql.Generate()
db.Query(sql, param)
Output:
SELECT a, b FROM table_1 WHERE a = 1 LIMIT 1 UNION SELECT a, b FROM table_2 WHERE a = 1 LIMIT 1
You can see the difference between a union query and a standard select. In a union, you must use a JSON array with the standard JSON format as before.
withUnion bool
withSanitizedInjection bool
withUnion: It is used to set the query to union and the structure must be of array type.
withSanitizedInjection: This is an experimental feature, it is far from perfect, and it serves to validate SQL strings against SQL Injection.
const (
Equal SQLOperatorEnum = "="
NotEqual SQLOperatorEnum = "<>"
LessThan SQLOperatorEnum = "<"
LessEqual SQLOperatorEnum = "<="
GreaterThan SQLOperatorEnum = ">"
GreaterEqual SQLOperatorEnum = ">="
Like SQLOperatorEnum = "LIKE"
Ilike SQLOperatorEnum = "ILIKE"
Between SQLOperatorEnum = "BETWEEN"
NotLike SQLOperatorEnum = "NOT LIKE"
In SQLOperatorEnum = "IN"
NotIn SQLOperatorEnum = "NOT IN"
IsNull SQLOperatorEnum = "IS NULL"
IsNotNull SQLOperatorEnum = "IS NOT NULL"
)
const (
Boolean SQLDataTypeEnum = "BOOLEAN"
String SQLDataTypeEnum = "STRING"
Number SQLDataTypeEnum = "NUMBER"
Raw SQLDataTypeEnum = "RAW"
Function SQLDataTypeEnum = "FUNCTION"
Array SQLDataTypeEnum = "ARRAY"
)
-
isStatic: (boolean) isStatic is used to set the value of the clause. If true, the value will not be parsed to parameters. However, if false, the opposite will occur. This is typically used in where statements.
-
isField: (boolean) isField is used to set a field so that it does not use single quotes. For example, if you describe a function and do not use isField, it will look like this: COUNT('field'). However, if you use isField, it will look like this: COUNT(field).
In general, the structure of the JSON format used is as follows:
-
table: Used to describe the table name, e.g. table_name (string)
-
selectFields: Used to select fields from a table, this property uses the Array type, you can combine Array of String, and Array of Json, the example is as follows:
{ "selectFields": [ "table_1.a", { "field": "table_1.b", "alias": "foo_bar" }, { "field": "table_2.a", "alias": "baz", "subquery": { "table": "table_4", "selectFields": ["*"], "conditions": [ { "datatype": "number", "clause": "a", "operator": "=", "value": 1 } ], "limit": 1 } }, "table_2.b", "table_3.a", "table_3.b" ] }
There you can see there is a subquery, you can use a subquery with the same format as its parent, you can also describe a field with an alias in the selection field.
NOTE: If you are using a subquery, you don't need to describe the datatype property
-
join: You can use join to combine multiple tables, an example is as follows:
{ "join": [ { "table": "table_2", "type": "join", "on": { "table_2.a": "table_1.a" } }, { "table": "table_3", "type": "left", "on": { "table_3.a": "table_2.a" } } ] }
-
conditions: Conditions are used for SQL Where clauses. The structure of these conditions is dynamic; you can use a function, subquery, or composite. Consider the following example:
{ "conditions": [ { "datatype": "string", "clause": "table_1.a", "operator": "=", "value": "foo" }, { "operand": "and", "datatype": "boolean", "clause": "table_1.b", "operator": "=", "value": true }, { "operand": "and", "datatype": "function", "clause": "table_2.a", "operator": ">", "value": { "sqlFunc": { "name": "sum", "params": [100] } } }, { "operand": "and", "clause": "table_2.b", "operator": "=", "value": { "subquery": { "table": "table_4", "selectFields": ["*"], "conditions": [ { "datatype": "number", "clause": "a", "operator": "=", "value": 1 } ], "limit": 1 } } }, { "operand": "or", "composite": [ { "clause": "table_3.a", "datatype": "string", "operator": "between", "value": { "from": "2020-01-01", "to": "2023-01-01" } }, { "operand": "and", "datatype": "string", "clause": "table_3.b", "operator": "=", "value": "2" } ] } ] }
-
groupBy:
{ "groupBy": { "fields": ["table_1.a"] } }
-
having
{ "having": [ { "clause": { "sqlFunc": { "name": "count", "isField": true, "params": ["table_2.a"] } }, "datatype": "number", "operator": ">", "value": 10 } ] }
-
orderBy, limit, offset:
{ "orderBy": { "fields": ["table_1.a", "table_2.a"], "sort": "asc" }, "limit": 1, "offset": 0 }
Or you can describe the limit and offset like this
{ "offset": { "isStatic": true, "value": 10 }, "offset": { "value": 10 } }
You can also convert to raw query without parameters.
jql, err := gojson2sql.NewJson2Sql([]byte(sqlJson), &gojson2sql.Json2SqlConf{})
if err != nil {
panic(err)
}
sql := jql.Build()
fmt.Println("SQL:", sql)
Output:
SQL: SELECT a, b FROM table_1 WHERE a = 1 LIMIT 1
sqlJson := `
{
"table": "table_1",
"selectFields": [
"table_1.a",
{
"field": "table_1.b",
"alias": "foo_bar"
},
{
"field": "table_2.a",
"alias": "baz",
"subquery": {
"table": "table_4",
"selectFields": ["*"],
"conditions": [
{
"datatype": "number",
"clause": "a",
"operator": "=",
"value": 1
}
],
"limit": 1
}
},
"table_2.b",
"table_3.a",
"table_3.b"
],
"join": [
{
"table": "table_2",
"type": "join",
"on": {
"table_2.a": "table_1.a"
}
},
{
"table": "table_3",
"type": "left",
"on": {
"table_3.a": "table_2.a"
}
}
],
"conditions": [
{
"datatype": "string",
"clause": "table_1.a",
"operator": "=",
"value": "foo"
},
{
"operand": "and",
"datatype": "boolean",
"clause": "table_1.b",
"operator": "=",
"value": true
},
{
"operand": "and",
"datatype": "function",
"clause": "table_2.a",
"operator": ">",
"value": {
"sqlFunc": {
"name": "sum",
"params": [100]
}
}
},
{
"operand": "and",
"clause": "table_2.b",
"operator": "=",
"value": {
"subquery": {
"table": "table_4",
"selectFields": ["*"],
"conditions": [
{
"datatype": "number",
"clause": "a",
"operator": "=",
"value": 1
}
],
"limit": 1
}
}
},
{
"operand": "or",
"composite": [
{
"clause": "table_3.a",
"datatype": "string",
"operator": "between",
"value": {
"from": "2020-01-01",
"to": "2023-01-01"
}
},
{
"operand": "and",
"datatype": "string",
"clause": "table_3.b",
"operator": "=",
"value": "2"
}
]
}
],
"groupBy": {
"fields": ["table_1.a"]
},
"having": [
{
"clause": {
"sqlFunc": {
"name": "count",
"isField": true,
"params": ["table_2.a"]
}
},
"datatype": "number",
"operator": ">",
"value": 10
}
],
"orderBy": {
"fields": ["table_1.a", "table_2.a"],
"sort": "asc"
},
"limit": 1,
"offset": 0
}
`
jql, err := gojson2sql.NewJson2Sql([]byte(sqlJson), &gojson2sql.Json2SqlConf{})
if err != nil {
panic(err)
}
sql, param, _ := jql.Generate()
fmt.Println("SQL:", sql)
fmt.Println("Param:", param)
output:
SQL:
SELECT
table_1.a,
table_1.b AS foo_bar,
(SELECT * FROM table_4 WHERE a = ? LIMIT 1) AS baz,
table_2.b,
table_3.a,
table_3.b
FROM table_1
JOIN table_2 ON table_2.a = table_1.a
LEFT JOIN table_3 ON table_3.a = table_2.a
WHERE
table_1.a = ? AND
table_1.b = ? AND
table_2.a > sum(?) AND
table_2.b = (SELECT * FROM table_4 WHERE a = ? LIMIT 1) OR
(table_3.a BETWEEN ? AND ? AND table_3.b = ?)
GROUP BY table_1.a
HAVING COUNT(table_2.a) > ?
ORDER BY table_1.a, table_2.a ASC
LIMIT 1
OFFSET 0
Param:
[1 foo true 100 1 2020-01-01 2023-01-01 2 10]
{
"table": "table_1",
"selectFields": [
{
"field": "table_1.a",
"alias": "foo_bar"
},
{
"alias": "foo_bar_baz",
"addFunction": {
"sqlFunc": {
"name": "count",
"isField": true,
"params": ["table_1.b"]
}
}
},
{
"field": "table_2.a",
"alias": "baz",
"subquery": {
"table": "table_4",
"selectFields": ["*"],
"conditions": [
{
"datatype": "number",
"clause": "a",
"operator": "=",
"value": 1
}
],
"limit": 1
}
},
{
"when": [
{
"clause": "table_2.b",
"datatype": "number",
"isStatic": true,
"operator": ">",
"value": 100,
"expectation": {
"datatype": "BOOLEAN",
"isStatic": true,
"value": true
}
}
],
"defaultValue": {
"isStatic": true,
"value": {
"subquery": {
"table": "table_3",
"selectFields": ["*"],
"conditions": [
{
"datatype": "NUMBER",
"clause": "a",
"operator": "=",
"value": 1
}
],
"limit": 1
}
}
},
"alias": "field_alias"
},
"table_3.a",
"table_3.b"
],
"join": [
{
"table": "table_2",
"type": "join",
"on": {
"table_2.a": "table_1.a"
}
},
{
"table": "table_3",
"type": "left",
"on": {
"table_3.a": "table_2.a"
}
}
],
"conditions": [
{
"datatype": "string",
"clause": "table_1.a",
"operator": "=",
"value": "foo"
},
{
"operand": "and",
"datatype": "boolean",
"clause": "table_1.b",
"operator": "=",
"value": true
},
{
"operand": "and",
"datatype": "function",
"clause": "table_2.a",
"operator": ">",
"value": {
"sqlFunc": {
"name": "sum",
"params": [100]
}
}
},
{
"operand": "and",
"clause": "table_2.b",
"operator": "=",
"value": {
"subquery": {
"table": "table_4",
"selectFields": ["*"],
"conditions": [
{
"datatype": "number",
"clause": "a",
"operator": "=",
"value": 1
}
],
"limit": 1
}
}
},
{
"operand": "or",
"composite": [
{
"clause": "table_3.a",
"datatype": "string",
"operator": "between",
"value": {
"from": "2020-01-01",
"to": "2023-01-01"
}
},
{
"operand": "and",
"datatype": "string",
"clause": "table_3.b",
"operator": "=",
"value": "2"
}
]
}
],
"groupBy": {
"fields": ["table_1.a"]
},
"having": [
{
"clause": {
"sqlFunc": {
"name": "count",
"isField": true,
"params": ["table_2.a"]
}
},
"datatype": "number",
"operator": ">",
"value": 10
}
],
"orderBy": {
"fields": ["table_1.a", "table_2.a"],
"sort": "asc"
},
"limit": {
"isStatic": true,
"value": 10
},
"offset": 0
}
output:
SELECT
table_1.a AS foo_bar,
COUNT(table_1.b) AS foo_bar_baz,
(SELECT * FROM table_4 WHERE a = 1 LIMIT 1) AS baz,
CASE
WHEN table_2.b > 100 THEN true
ELSE (SELECT * FROM table_3 WHERE a = 1 LIMIT 1)
END AS field_alias,
table_3.a,
table_3.b
FROM table_1
JOIN table_2 ON table_2.a = table_1.a
LEFT JOIN table_3 ON table_3.a = table_2.a
WHERE
table_1.a = 'foo' AND
table_1.b = true AND
table_2.a > sum(100) AND
table_2.b = (SELECT * FROM table_4 WHERE a = 1 LIMIT 1) OR
(table_3.a BETWEEN '2020-01-01' AND '2023-01-01' AND table_3.b = '2')
GROUP BY table_1.a
HAVING
COUNT(table_2.a) > 10
ORDER BY table_1.a, table_2.a ASC
LIMIT 10
OFFSET 0
> go test -v -cover ./...
=== RUN TestConstructor
--- PASS: TestConstructor (0.00s)
=== RUN TestConstructor_Fail
--- PASS: TestConstructor_Fail (0.00s)
=== RUN TestConstructor_Fail_Union
--- PASS: TestConstructor_Fail_Union (0.00s)
=== RUN TestRawJson_OK
--- PASS: TestRawJson_OK (0.00s)
=== RUN TestRawJson_Error
--- PASS: TestRawJson_Error (0.00s)
=== RUN TestMaskedQueryValue
--- PASS: TestMaskedQueryValue (0.00s)
=== RUN TestGenerateSelectFrom
--- PASS: TestGenerateSelectFrom (0.00s)
=== RUN TestGenerateSelectFrom_Selection
--- PASS: TestGenerateSelectFrom_Selection (0.00s)
=== RUN TestGenerateSelectFrom_CaseWhenThen
--- PASS: TestGenerateSelectFrom_CaseWhenThen (0.00s)
=== RUN TestGenerateSelectFrom_CaseDefaultValueSub
--- PASS: TestGenerateSelectFrom_CaseDefaultValueSub (0.00s)
=== RUN TestGenerateSelectFrom_SqlFunc
--- PASS: TestGenerateSelectFrom_SqlFunc (0.00s)
=== RUN TestSqlLikeAndBlankDatatype
--- PASS: TestSqlLikeAndBlankDatatype (0.00s)
=== RUN TestSqlLikeWithOperand
--- PASS: TestSqlLikeWithOperand (0.00s)
=== RUN TestBetweenWithOperand
--- PASS: TestBetweenWithOperand (0.00s)
=== RUN TestCompositeWithoutOperand
--- PASS: TestCompositeWithoutOperand (0.00s)
=== RUN TestGenerateOrderBy
--- PASS: TestGenerateOrderBy (0.00s)
=== RUN TestGenerateOrderBy_WithSort
--- PASS: TestGenerateOrderBy_WithSort (0.00s)
=== RUN TestGenerateGroupBy
--- PASS: TestGenerateGroupBy (0.00s)
=== RUN TestGenerateJoin_JOIN
--- PASS: TestGenerateJoin_JOIN (0.00s)
=== RUN TestGenerateJoin_INNER_JOIN
--- PASS: TestGenerateJoin_INNER_JOIN (0.00s)
=== RUN TestGenerateJoin_LEFT_JOIN
--- PASS: TestGenerateJoin_LEFT_JOIN (0.00s)
=== RUN TestGenerateJoin_RIGHT_JOIN
--- PASS: TestGenerateJoin_RIGHT_JOIN (0.00s)
=== RUN TestGenerateHaving
--- PASS: TestGenerateHaving (0.00s)
=== RUN TestGenerateWhere
--- PASS: TestGenerateWhere (0.00s)
=== RUN TestGenerateConditions
--- PASS: TestGenerateConditions (0.00s)
=== RUN TestGenerateConditions_SubQuery
--- PASS: TestGenerateConditions_SubQuery (0.00s)
=== RUN TestLimit_Static
--- PASS: TestLimit_Static (0.00s)
=== RUN TestLimit_ToParam
--- PASS: TestLimit_ToParam (0.00s)
=== RUN TestOffset_Static
--- PASS: TestOffset_Static (0.00s)
=== RUN TestOffset_ToParam
--- PASS: TestOffset_ToParam (0.00s)
=== RUN TestBuildJsonToSql
--- PASS: TestBuildJsonToSql (0.00s)
=== RUN TestGenerateJsonToSql
--- PASS: TestGenerateJsonToSql (0.00s)
=== RUN TestBuildRawUnion
--- PASS: TestBuildRawUnion (0.00s)
=== RUN TestGenerateUnion
--- PASS: TestGenerateUnion (0.00s)
=== RUN TestGenerateBuild_PreventInjection
--- PASS: TestGenerateBuild_PreventInjection (0.00s)
=== RUN TestGenerate_PreventInjection
--- PASS: TestGenerate_PreventInjection (0.00s)
=== RUN TestGenerateBuildUnion_PreventInjection
--- PASS: TestGenerateBuildUnion_PreventInjection (0.00s)
=== RUN TestGenerateUnion_PreventInjection
--- PASS: TestGenerateUnion_PreventInjection (0.00s)
=== RUN TestIsValidDataType
--- PASS: TestIsValidDataType (0.00s)
=== RUN TestGetValueFromDataType
--- PASS: TestGetValueFromDataType (0.00s)
=== RUN TestCheckArrayType
--- PASS: TestCheckArrayType (0.00s)
=== RUN TestArrayConversionToStringExpression
--- PASS: TestArrayConversionToStringExpression (0.00s)
=== RUN TestExtractValueByDataType
--- PASS: TestExtractValueByDataType (0.00s)
=== RUN TestGetSqlExpression
--- PASS: TestGetSqlExpression (0.00s)
=== RUN TestIsValidOperator
--- PASS: TestIsValidOperator (0.00s)
=== RUN TestGetValueFromOperator
--- PASS: TestGetValueFromOperator (0.00s)
PASS
coverage: 100.0% of statements
Specs:
- MacBook Pro M1 (2020)
- 8-Cores (arm64)
- 8GB of RAM
> go test -bench=. -benchmem
goos: darwin
goarch: arm64
pkg: github.com/bonkzero404/gojson2sql
BenchmarkJson2Sql_BuildRaw-8 29390 39416 ns/op 29289 B/op 507 allocs/op
BenchmarkJson2Sql_Generate-8 17575 67617 ns/op 41164 B/op 607 allocs/op
BenchmarkJson2Sql_Union_BuildRaw-8 13789 86378 ns/op 65804 B/op 1016 allocs/op
BenchmarkJson2Sql_Union_Generate-8 8659 131424 ns/op 76072 B/op 1143 allocs/op
BenchmarkJson2Sql_BuildRaw_WithSanitizedSQLi-8 8269 146946 ns/op 49644 B/op 627 allocs/op
BenchmarkJson2Sql_Generate_WithSanitizedSQLi-8 5640 211519 ns/op 61368 B/op 727 allocs/op
BenchmarkJson2Sql_Union_BuildRaw_WithSanitizedSQLi-8 4190 282480 ns/op 87426 B/op 1137 allocs/op
BenchmarkJson2Sql_Union_Generate_WithSanitizedSQLi-8 2959 401434 ns/op 97889 B/op 1263 allocs/op