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

[T2580] Fix handling null values in mysql #543

Merged
Merged
Show file tree
Hide file tree
Changes from 4 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.93.0 - 2022-05-23
- Handle properly null values in MySQL

# 0.93.0 - 2022-05-18
- Reset placeholders in a connection state after `ReadyForQuery` packet.

Expand Down
3 changes: 3 additions & 0 deletions crypto/envelope_detector.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,9 @@ func (wrapper *OldContainerDetectorWrapper) OnCryptoEnvelope(ctx context.Context

// OnColumn callback which finds serializedContainer or AcraStruct/AcraBlock for backward compatibility
func (wrapper *OldContainerDetectorWrapper) OnColumn(ctx context.Context, inBuffer []byte) (context.Context, []byte, error) {
if inBuffer == nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

@Lagovas Can we place this check on the level of EnvelopeDetector? We are already doing some inBuffer checking here

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

but we return there inBuffer as is. Without allocating new buffer. If inBuffer in OnColumn will be nil, it will returned as is due to if len(inBuffer) < SerializedContainerMinSize

Copy link
Contributor

Choose a reason for hiding this comment

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

Got it;

return ctx, inBuffer, nil
}
// we should track that if incoming data contains any signs of new container and if it is we return data as is
// otherwise try to search for AcraBlock or AcraStruct to save backward compatibility
// so before any OnColumn we should reset hasMatchedEnvelope flag to track if its changed during EnvelopeDetector OnColumn via BackWrapper.OnCryptoEnvelope callback
Expand Down
10 changes: 8 additions & 2 deletions decryptor/mysql/response_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -513,8 +513,14 @@ func (handler *Handler) processTextDataRow(ctx context.Context, rowData []byte,
if err != nil {
return nil, err
}

decrCtx, value, err := handler.onColumnDecryption(ctx, i, value, false, fields[i])
var decrCtx context.Context
// skip processing if value is NULL/nil
if value == nil {
output = append(output, rowData[pos:pos+n]...)
pos += n
continue
}
decrCtx, value, err = handler.onColumnDecryption(ctx, i, value, false, fields[i])
if err != nil {
fieldLogger.WithField(logging.FieldKeyEventCode, logging.EventCodeErrorGeneral).
WithError(err).Errorln("Failed to process column data")
Expand Down
47 changes: 13 additions & 34 deletions tests/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5654,13 +5654,8 @@ def testEmptyValues(self):
# check null values
result = self.engine1.execute(sa.select([self.temp_table]).where(self.temp_table.c.id == null_value_id))
row = result.fetchone()
if TEST_MYSQL:
# PyMySQL returns empty strings for NULL values
self.assertEqual(row['text'], '')
self.assertEqual(row['binary'], b'')
else:
self.assertIsNone(row['text'])
self.assertIsNone(row['binary'])
self.assertIsNone(row['text'])
self.assertIsNone(row['binary'])

# check empty values
result = self.engine1.execute(sa.select([self.temp_table]).where(self.temp_table.c.id == empty_value_id))
Expand Down Expand Up @@ -8647,12 +8642,14 @@ def testClientIDRead(self):
'value_int64': 64,
'value_bytes': b'value_bytes',
'value_str': 'value_str',
'value_empty_str': ''
'value_empty_str': '',
'value_null_str': None,
'value_null_int32': None,
}

self.schema_table.create(bind=self.engine_raw, checkfirst=True)
columns = ('value_bytes', 'value_int32', 'value_int64', 'value_empty_str', 'value_str')
null_columns = ('value_null_str', 'value_null_int32')
columns = ('value_bytes', 'value_int32', 'value_int64', 'value_empty_str', 'value_str', 'value_null_str',
'value_null_int32')

self.engine1.execute(self.test_table.insert(), data)

Expand All @@ -8665,18 +8662,12 @@ def testClientIDRead(self):
self.assertEqual(data[column], row[column])
self.assertIsInstance(row[column], type(data[column]))

# mysql.connector represent null value as empty string
for column in null_columns:
self.assertEqual(row[column], '')

row = self.executor2.execute(query)[0]
for column in columns:
self.assertEqual(row[column], default_expected_values[column])
self.assertIsInstance(row[column], type(default_expected_values[column]))

for column in null_columns:
self.assertEqual(row[column], '')

row = self.engine_raw.execute(sa.select([self.test_table])
.where(self.test_table.c.id == data['id'])).fetchone()
for column in columns:
Expand Down Expand Up @@ -8959,8 +8950,8 @@ def testClientIDRead(self):
}
self.schema_table.create(bind=self.engine_raw, checkfirst=True)
self.engine1.execute(self.test_table.insert(), data)
columns = ('value_str', 'value_bytes', 'value_int32', 'value_int64', 'value_empty_str')
null_columns = ('value_null_str', 'value_null_int32')
columns = ('value_str', 'value_bytes', 'value_int32', 'value_int64', 'value_empty_str', 'value_null_str',
'value_null_int32')

compile_kwargs = {"literal_binds": True}
query = sa.select([self.test_table]).where(self.test_table.c.id == data['id'])
Expand All @@ -8971,10 +8962,6 @@ def testClientIDRead(self):
self.assertEqual(data[column], row[column])
self.assertIsInstance(row[column], type(data[column]))

# mysql.connector represent null value as empty string
for column in null_columns:
self.assertEqual(row[column], '')

# field types should be rollbacked in case of invalid encoding
row = self.executor2.execute(query)[0]

Expand Down Expand Up @@ -9497,8 +9484,8 @@ def testClientIDRead(self):
}
self.schema_table.create(bind=self.engine_raw, checkfirst=True)
self.engine1.execute(self.test_table.insert(), data)
columns = ('value_str', 'value_bytes', 'value_int32', 'value_int64', 'value_empty_str')
null_columns = ('value_null_str', 'value_null_int32')
columns = ('value_str', 'value_bytes', 'value_int32', 'value_int64', 'value_empty_str', 'value_null_str',
'value_null_int32')

compile_kwargs = {"literal_binds": True}
query = sa.select([self.test_table]).where(self.test_table.c.id == data['id'])
Expand All @@ -9509,10 +9496,6 @@ def testClientIDRead(self):
self.assertEqual(data[column], row[column])
self.assertIsInstance(row[column], type(data[column]))

# mysql.connector represent null value as empty string
for column in null_columns:
self.assertEqual(row[column], '')

# field types should be rollbacked in case of invalid encoding
row = self.executor2.execute(query)[0]

Expand Down Expand Up @@ -9649,8 +9632,8 @@ def testClientIDRead(self):
}
self.schema_table.create(bind=self.engine_raw, checkfirst=True)
self.engine1.execute(self.test_table.insert(), data)
columns = ('value_str', 'value_bytes', 'value_int32', 'value_int64', 'value_empty_str')
null_columns = ('value_null_str', 'value_null_int32')
columns = ('value_str', 'value_bytes', 'value_int32', 'value_int64', 'value_empty_str', 'value_null_str',
'value_null_int32')

compile_kwargs = {"literal_binds": True}
query = sa.select([self.test_table]).where(self.test_table.c.id == data['id'])
Expand All @@ -9661,10 +9644,6 @@ def testClientIDRead(self):
self.assertEqual(data[column], row[column])
self.assertIsInstance(row[column], type(data[column]))

# mysql.connector represent null value as empty string
for column in null_columns:
self.assertEqual(row[column], '')

# we expect an exception because of decryption error
with self.assertRaises(mysql.connector.errors.DatabaseError) as ex:
self.executor2.execute(query)[0]
Expand Down