Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle null::text casts in postgresql dialect, fix panics #479

Merged
merged 1 commit into from
Jan 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG_DEV.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## 0.92.0 - 2021-12-29
- Improve sqlparser, support `null::text` type casts and avoid panics.

## 0.92.0 - 2021-12-22
- Extend python examples, add mysql support
- Generate certificates with service names as additional SAN for docker-compose files. Extend bash script to support several
Expand Down
17 changes: 14 additions & 3 deletions sqlparser/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -1221,21 +1221,32 @@ const (
ValArg
BitVal
PgEscapeString
Casted
PgPlaceholder
// Use this type for casted values that are different from SQLVal.
// because we have existing logic of comparison and formatting SQLVal
// and it easier to extend this one instead of propagating postgresql value conversion
// to all kinds of Expr. Maybe in a future when we will know more cases when it useful for
// our data manipulation, we will refactor it. Now it's just to support generic queries and avoid
// a lot of refactoring of sql.y and SQL grammar
UnknownVal
)

// SQLVal represents a single value.
type SQLVal struct {
Type ValType
Val []byte
CastType []byte
unknown Expr
}

// NewCastVal builds new CastVal
func NewCastVal(val Expr, castType []byte) *SQLVal {
v := val.(*SQLVal)
return &SQLVal{Type: v.Type, Val: v.Val, CastType: castType}
switch v := val.(type) {
case *SQLVal:
return &SQLVal{Type: v.Type, Val: v.Val, CastType: castType}
default:
return &SQLVal{unknown: val, Type: UnknownVal, CastType: castType}
}
}

// ErrInvalidStringLiteralQuotes if used string token as literal with incorrect quotes
Expand Down
2 changes: 2 additions & 0 deletions sqlparser/ast_methods.go
Original file line number Diff line number Diff line change
Expand Up @@ -1339,6 +1339,8 @@ func (node *SQLVal) Format(buf *TrackedBuffer) {
buf.Myprintf("E'%s'", sqltypes.EncodeBytesSQLWithoutQuotes(node.Val))
case PgPlaceholder:
buf.Myprintf("%s", []byte(node.Val))
case UnknownVal:
node.unknown.Format(buf)
default:
panic("unexpected")
}
Expand Down
27 changes: 27 additions & 0 deletions sqlparser/ast_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -679,3 +679,30 @@ func TestEmptyQuery(t *testing.T) {
}
}
}

func TestUnknownCastVal(t *testing.T) {
type testcase struct {
expr Expr
castType []byte
resultVal []byte
}
testcases := []testcase{
{
expr: &NullVal{},
castType: []byte(`::text`),
resultVal: nil,
},
}
for _, tcase := range testcases {
value := NewCastVal(tcase.expr, tcase.castType)
if value.unknown != tcase.expr {
t.Fatal("Unknown value != source expression")
}
if !bytes.Equal(value.Val, tcase.resultVal) {
t.Fatal("SQLVal.Val != expected value")
}
if !bytes.Equal(value.CastType, tcase.castType) {
t.Fatal("SQLVal.CastType != expected cast type")
}
}
}
3 changes: 3 additions & 0 deletions sqlparser/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1315,6 +1315,9 @@ var (
}, {
input: "drop database if exists test_db",
output: "drop database test_db",
}, {
input: "select NULL::text from dual",
output: "select null::text from dual",
}}
)

Expand Down
9 changes: 0 additions & 9 deletions tests/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8762,15 +8762,6 @@ def test_invalid_specified_values(self):
self.assertTrue(found, "Not found any expected exception")


class TestPanicRecover(AcraCatchLogsMixin, BaseTestCase):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no more test? 🥺

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it was one known case that caused panics. I don't know other cases to put here *( We can leave this test empty, but IMHO, we can write it one more time when we will need it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i thought this PR means that panics won't happen for null:text cast, so the test should always pass now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And after this fix it doesn't panic anymore )

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i thought this PR means that panics won't happen for null:text cast, so the test should always pass now.

oh, you wrote it before me)) 10 sec difference) Yes, it fixes and doesn't panic anymore

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay, thank you for the explanation! great job!

def testRecovering(self):
with self.assertRaises(DatabaseError):
# call unsupported query by sqlparser
self.engine1.execute('select null::text from dual;')

self.assertIn('panic in connection processing, close connection', self.read_log(self.acra).lower())


class TestRegressionInvalidOctalEncoding(BaseTokenizationWithBinaryPostgreSQL):
def testOctalIntegerValue(self):
default_client_id_table = sa.Table(
Expand Down