diff --git a/generator/postgres/postgres_generator.go b/generator/postgres/postgres_generator.go index 08bdb8d..e413abe 100644 --- a/generator/postgres/postgres_generator.go +++ b/generator/postgres/postgres_generator.go @@ -80,7 +80,7 @@ func GenerateDB(db *sql.DB, schema, destDir string, templates ...template.Templa err = template.ProcessSchema(destDir, schemaMetadata, generatorTemplate) if err != nil { - return fmt.Errorf("failed to generate schema %s: %d", schemaMetadata.Name, err) + return fmt.Errorf("failed to generate schema %s: %w", schemaMetadata.Name, err) } return nil diff --git a/internal/jet/sql_builder.go b/internal/jet/sql_builder.go index 9471911..a7a6abc 100644 --- a/internal/jet/sql_builder.go +++ b/internal/jet/sql_builder.go @@ -112,7 +112,8 @@ func (s *SQLBuilder) WriteString(str string) { // WriteJsonObjKey serializes json object key func (s *SQLBuilder) WriteJsonObjKey(key string) { - s.WriteString(fmt.Sprintf(`'%s', `, key)) + s.WriteString(stringQuote(key)) + s.WriteString(", ") } // WriteIdentifier adds identifier to output SQL diff --git a/internal/jet/sql_builder_test.go b/internal/jet/sql_builder_test.go index fc6e326..71424f7 100644 --- a/internal/jet/sql_builder_test.go +++ b/internal/jet/sql_builder_test.go @@ -45,6 +45,14 @@ func TestArgToString(t *testing.T) { }() } +func TestWriteJsonObjKeyEscapesStringLiteral(t *testing.T) { + s := &SQLBuilder{Dialect: defaultDialect} + + s.WriteJsonObjKey("author's name") + + require.Equal(t, "'author''s name', ", s.Buff.String()) +} + func TestFallTrough(t *testing.T) { require.Equal(t, FallTrough([]SerializeOption{ShortName}), []SerializeOption{ShortName}) require.Equal(t, FallTrough([]SerializeOption{SkipNewLine}), []SerializeOption(nil)) diff --git a/qrm/qrm.go b/qrm/qrm.go index cf44044..e09d2d9 100644 --- a/qrm/qrm.go +++ b/qrm/qrm.go @@ -131,14 +131,12 @@ func queryJson(ctx context.Context, db Queryable, query string, args []interface return 1, err } - if jsonData == nil { - return 1, nil - } + if jsonData != nil { + err = GlobalConfig.JsonUnmarshalFunc(jsonData, &destPtr) - err = GlobalConfig.JsonUnmarshalFunc(jsonData, &destPtr) - - if err != nil { - return 1, fmt.Errorf("jet: invalid json, %w", err) + if err != nil { + return 1, fmt.Errorf("jet: invalid json, %w", err) + } } if rows.Next() { diff --git a/stmtcache/tx.go b/stmtcache/tx.go index c02fb6b..0076f46 100644 --- a/stmtcache/tx.go +++ b/stmtcache/tx.go @@ -57,7 +57,7 @@ func (t *Tx) QueryContext(ctx context.Context, query string, args ...interface{} return nil, err } - return prepStmt.Query(args...) + return prepStmt.QueryContext(ctx, args...) } // Prepare delegates call to PrepareContext using context.Background as a parameter. diff --git a/tests/mysql/select_json_test.go b/tests/mysql/select_json_test.go index d2994db..d18623c 100644 --- a/tests/mysql/select_json_test.go +++ b/tests/mysql/select_json_test.go @@ -505,3 +505,47 @@ func TestSelectJson_ProjectionNotAliased(t *testing.T) { }, "jet: expression need to be aliased when used as SELECT JSON projection.") }) } + +func TestSelectJsonObject_EscapesJsonKeys(t *testing.T) { + stmt := SELECT_JSON_OBJ( + String("value").AS("author"), + String("value").AS("author's name"), + String("value").AS("author''s name"), + String("value").AS("author \"name\""), + String("value").AS(`C:\tmp\file`), + String("value").AS("hello\nworld"), + String("value").AS("a'b\\\\c\\nd\\r\\x00e\\x1af"), + String("value").AS("žika 😀"), + ) + + testutils.AssertDebugStatementSql(t, stmt, ` +SELECT JSON_OBJECT( + 'author', 'value', + 'author''s name', 'value', + 'author''''s name', 'value', + 'author "name"', 'value', + 'C:\tmp\file', 'value', + 'hello +world', 'value', + 'a''b\\c\nd\r\x00e\x1af', 'value', + 'žika 😀', 'value' + ) AS "json"; +`) + + var dest map[string]any + + err := stmt.QueryContext(ctx, db, &dest) + require.NoError(t, err) + testutils.AssertJSON(t, dest, ` +{ + "C:\tmpfile": "value", + "a'b\\c\nd\rx00ex1af": "value", + "author": "value", + "author \"name\"": "value", + "author''s name": "value", + "author's name": "value", + "hello\nworld": "value", + "žika 😀": "value" +} +`) +} diff --git a/tests/postgres/select_json_test.go b/tests/postgres/select_json_test.go index 634f93a..b03b814 100644 --- a/tests/postgres/select_json_test.go +++ b/tests/postgres/select_json_test.go @@ -914,3 +914,55 @@ func TestSelectJson_InvalidJson(t *testing.T) { err := stmt.QueryContext(ctx, db, &dest) require.ErrorContains(t, err, "invalid json") } + +func TestSelectJsonObject_EscapesJsonKeys(t *testing.T) { + stmt := SELECT_JSON_OBJ( + String("value").AS("author"), + String("value").AS("author's name"), + String("value").AS("author''s name"), + String("value").AS(`C:\tmp\file`), + String("value").AS("hello\nworld"), + String("value").AS("a'b\\\\c\\nd\\r\\x00e\\x1af"), + String("value").AS("žika 😀"), + ) + + testutils.AssertDebugStatementSql(t, stmt, ` +SELECT row_to_json(records) AS "json" +FROM ( + SELECT 'value'::text AS "author", + 'value'::text AS "author's name", + 'value'::text AS "author''s name", + 'value'::text AS "C:\tmp\file", + 'value'::text AS "hello +world", + 'value'::text AS "a'b\\c\nd\r\x00e\x1af", + 'value'::text AS "žika 😀" + ) AS records; +`) + + var dest map[string]any + + err := stmt.QueryContext(ctx, db, &dest) + require.NoError(t, err) + testutils.AssertJSON(t, dest, ` +{ + "C:\\tmp\\file": "value", + "a'b\\\\c\\nd\\r\\x00e\\x1af": "value", + "author": "value", + "author''s name": "value", + "author's name": "value", + "hello\nworld": "value", + "žika 😀": "value" +} +`) +} + +func TestSelectJsonObject_NullMoreThanOneRow(t *testing.T) { + var dest map[string]any + + _, err := qrm.QueryJsonObj(ctx, db, ` +SELECT NULL::json AS "json" +UNION ALL +SELECT NULL::json AS "json"`, nil, &dest) + require.ErrorContains(t, err, "jet: query returned more then one row") +}