Skip to content
Closed
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
18 changes: 14 additions & 4 deletions normalizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ type colonContext struct {
}

type groupablePlaceholder struct {
groupable bool
groupable bool
skippedCount int // number of (comma, placeholder) pairs skipped during grouping
}

type headState struct {
Expand Down Expand Up @@ -434,7 +435,9 @@ func (n *Normalizer) isObfuscatedValueGroupable(token *Token, lastValueToken *La
// we know it's the start of groupable placeholders
// we don't return here because we still need to write the first placeholder
groupablePlaceholder.groupable = true
groupablePlaceholder.skippedCount = 0
} else if lastValueToken.Value == "," && groupablePlaceholder.groupable {
groupablePlaceholder.skippedCount++
return true
}
}
Expand All @@ -446,14 +449,21 @@ func (n *Normalizer) isObfuscatedValueGroupable(token *Token, lastValueToken *La
if groupablePlaceholder.groupable && (token.Value == ")" || token.Value == "]") {
// end of groupable placeholders
groupablePlaceholder.groupable = false
groupablePlaceholder.skippedCount = 0
return false
}

if groupablePlaceholder.groupable && token.Value != NumberPlaceholder && token.Value != StringPlaceholder && lastValueToken != nil && lastValueToken.Value == "," {
// This is a tricky edge case. If we are inside a groupbale block, and the current token is not a placeholder,
// we not only want to write the current token to the normalizedSQLBuilder, but also write the last comma that we skipped.
// For example, (?, ARRAY[?, ?, ?]) should be normalized as (?, ARRAY[?])
// A non-placeholder was encountered while grouping. This means the parenthesized
// list contains mixed values (e.g. VALUES (?, ?, NOW(), ?)) and should not be grouped.
// Write back all the previously skipped (comma, placeholder) pairs and the trailing comma.
for i := 0; i < groupablePlaceholder.skippedCount; i++ {
normalizedSQLBuilder.WriteString(", ")
normalizedSQLBuilder.WriteString(NumberPlaceholder)
}
normalizedSQLBuilder.WriteString(lastValueToken.Value)
groupablePlaceholder.groupable = false
groupablePlaceholder.skippedCount = 0
return false
}

Expand Down
12 changes: 12 additions & 0 deletions obfuscate_and_normalize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,18 @@ multiline comment */
Size: 19,
},
},
{
// INSERT with mixed placeholders and function calls should preserve all value positions
input: "INSERT INTO t (a, b, c, d, e, f, g, h, i, j) VALUES ('x', 'y', 'z', 0, NOW(), NOW(), 1, 64, 18446744073709551615, '2026-04-11 00:00:00+00')",
expected: "INSERT INTO t ( a, b, c, d, e, f, g, h, i, j ) VALUES ( ?, ?, ?, ?, NOW ( ), NOW ( ), ?, ?, ?, ? )",
statementMetadata: StatementMetadata{
Tables: []string{"t"},
Comments: []string{},
Commands: []string{"INSERT"},
Procedures: []string{},
Size: 7,
},
},
}

obfuscator := NewObfuscator(
Expand Down
2 changes: 1 addition & 1 deletion testdata/mssql/insert/insert-identity-insert.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"input": "SET IDENTITY_INSERT orders ON; INSERT INTO orders (id, customer_id, order_date, status) VALUES (100, 3, GETDATE(), 'Pending'); SET IDENTITY_INSERT orders OFF;",
"outputs": [
{
"expected": "SET IDENTITY_INSERT orders ON; INSERT INTO orders ( id, customer_id, order_date, status ) VALUES ( ?, GETDATE ( ), ? ); SET IDENTITY_INSERT orders OFF",
"expected": "SET IDENTITY_INSERT orders ON; INSERT INTO orders ( id, customer_id, order_date, status ) VALUES ( ?, ?, GETDATE ( ), ? ); SET IDENTITY_INSERT orders OFF",
"statement_metadata": {
"size": 12,
"tables": ["orders"],
Expand Down
2 changes: 1 addition & 1 deletion testdata/mysql/insert/insert-blob-data.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"input": "INSERT INTO orders (customer_id, status, document) VALUES (5, 'Pending', LOAD_FILE('/path/to/file'));",
"outputs": [
{
"expected": "INSERT INTO orders ( customer_id, status, document ) VALUES ( ?, LOAD_FILE ( ? ) )",
"expected": "INSERT INTO orders ( customer_id, status, document ) VALUES ( ?, ?, LOAD_FILE ( ? ) )",
"statement_metadata": {
"size": 12,
"tables": ["orders"],
Expand Down
2 changes: 1 addition & 1 deletion testdata/mysql/insert/insert-spatial-data.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"input": "INSERT INTO orders (customer_id, status, location) VALUES (6, 'Delivered', ST_GeomFromText('POINT(1 1)'));",
"outputs": [
{
"expected": "INSERT INTO orders ( customer_id, status, location ) VALUES ( ?, ST_GeomFromText ( ? ) )",
"expected": "INSERT INTO orders ( customer_id, status, location ) VALUES ( ?, ?, ST_GeomFromText ( ? ) )",
"statement_metadata": {
"size": 12,
"tables": ["orders"],
Expand Down
2 changes: 1 addition & 1 deletion testdata/mysql/insert/insert-with-curdate-curtime.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"input": "INSERT INTO orders (customer_id, status, order_date, order_time) VALUES (15, 'Pending', CURDATE(), CURTIME());",
"outputs": [
{
"expected": "INSERT INTO orders ( customer_id, status, order_date, order_time ) VALUES ( ?, CURDATE ( ), CURTIME ( ) )",
"expected": "INSERT INTO orders ( customer_id, status, order_date, order_time ) VALUES ( ?, ?, CURDATE ( ), CURTIME ( ) )",
"statement_metadata": {
"size": 12,
"tables": ["orders"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"input": "INSERT INTO orders (customer_id, status, encrypted_note) VALUES (13, 'Pending', AES_ENCRYPT('Confidential note', 'encryption_key'));",
"outputs": [
{
"expected": "INSERT INTO orders ( customer_id, status, encrypted_note ) VALUES ( ?, AES_ENCRYPT ( ? ) )",
"expected": "INSERT INTO orders ( customer_id, status, encrypted_note ) VALUES ( ?, ?, AES_ENCRYPT ( ? ) )",
"statement_metadata": {
"size": 12,
"tables": ["orders"],
Expand Down
2 changes: 1 addition & 1 deletion testdata/mysql/insert/insert-with-spatial-data.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"input": "INSERT INTO orders (customer_id, status, location) VALUES (14, 'Pending', ST_GeomFromText('POINT(1 1)'));",
"outputs": [
{
"expected": "INSERT INTO orders ( customer_id, status, location ) VALUES ( ?, ST_GeomFromText ( ? ) )",
"expected": "INSERT INTO orders ( customer_id, status, location ) VALUES ( ?, ?, ST_GeomFromText ( ? ) )",
"statement_metadata": {
"size": 12,
"tables": ["orders"],
Expand Down
2 changes: 1 addition & 1 deletion testdata/mysql/insert/insert-with-timestamp.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"input": "INSERT INTO orders (customer_id, status, created_at) VALUES (4, 'Shipped', CURRENT_TIMESTAMP);",
"outputs": [
{
"expected": "INSERT INTO orders ( customer_id, status, created_at ) VALUES ( ?, CURRENT_TIMESTAMP )",
"expected": "INSERT INTO orders ( customer_id, status, created_at ) VALUES ( ?, ?, CURRENT_TIMESTAMP )",
"statement_metadata": {
"size": 12,
"tables": ["orders"],
Expand Down
2 changes: 1 addition & 1 deletion testdata/postgresql/insert/insert-with-default.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"input": "INSERT INTO products (name, price, description) VALUES ('New Product', 123, DEFAULT);",
"outputs": [
{
"expected": "INSERT INTO products ( name, price, description ) VALUES ( ?, DEFAULT )",
"expected": "INSERT INTO products ( name, price, description ) VALUES ( ?, ?, DEFAULT )",
"statement_metadata": {
"size": 14,
"tables": [
Expand Down
Loading