From ce4450080460aef42aec4f18026805642ba9c046 Mon Sep 17 00:00:00 2001 From: bibi samina Date: Thu, 27 Aug 2026 13:54:54 +0530 Subject: [PATCH 1/2] fix(django-spanner): backslash-escape string literals in quote_value --- .../django-google-spanner/django_spanner/schema.py | 5 ++++- .../tests/unit/django_spanner/test_schema.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/django-google-spanner/django_spanner/schema.py b/packages/django-google-spanner/django_spanner/schema.py index da57122bb73d..905e5843fe51 100644 --- a/packages/django-google-spanner/django_spanner/schema.py +++ b/packages/django-google-spanner/django_spanner/schema.py @@ -450,7 +450,10 @@ def add_index(self, model, index): def quote_value(self, value): # A more complete implementation isn't currently required. if isinstance(value, str): - return "'%s'" % value.replace("'", "''") + # GoogleSQL string literals use backslash escaping; '' quote + # doubling is not recognized, so escape the backslash first and + # then the quote (matching the db_default/generated inlining above). + return "'%s'" % value.replace("\\", "\\\\").replace("'", "\\'") if isinstance(value, bool): return "TRUE" if value else "FALSE" return str(value) diff --git a/packages/django-google-spanner/tests/unit/django_spanner/test_schema.py b/packages/django-google-spanner/tests/unit/django_spanner/test_schema.py index b7ef7cec39ec..0ce848f9afe3 100644 --- a/packages/django-google-spanner/tests/unit/django_spanner/test_schema.py +++ b/packages/django-google-spanner/tests/unit/django_spanner/test_schema.py @@ -40,6 +40,19 @@ def test_quote_value(self): schema_editor = DatabaseSchemaEditor(self.connection) self.assertEqual(schema_editor.quote_value(value=1.1), "1.1") + def test_quote_value_escapes_string(self): + """ + String literals must be backslash-escaped for GoogleSQL. A quote or + backslash in the value must not be able to terminate the literal. + """ + schema_editor = DatabaseSchemaEditor(self.connection) + self.assertEqual(schema_editor.quote_value(value="o'brien"), "'o\\'brien'") + self.assertEqual(schema_editor.quote_value(value="a\\b"), "'a\\\\b'") + self.assertEqual( + schema_editor.quote_value(value="\\'; DROP TABLE t; --"), + "'\\\\\\'; DROP TABLE t; --'", + ) + def test_skip_default(self): """ Tries skipping default as Cloud spanner doesn't support it. From e109b70737d5b4788c68f0b8320ffed19668d28a Mon Sep 17 00:00:00 2001 From: bibi samina Date: Sun, 13 Sep 2026 18:09:20 +0530 Subject: [PATCH 2/2] fix(django-spanner): escape newlines and handle None, bytes and dates in quote_value --- .../django_spanner/schema.py | 24 +++++++++- .../tests/unit/django_spanner/test_schema.py | 45 +++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/packages/django-google-spanner/django_spanner/schema.py b/packages/django-google-spanner/django_spanner/schema.py index 905e5843fe51..a90a56c72723 100644 --- a/packages/django-google-spanner/django_spanner/schema.py +++ b/packages/django-google-spanner/django_spanner/schema.py @@ -3,6 +3,7 @@ # Use of this source code is governed by a BSD-style # license that can be found in the LICENSE file or at # https://developers.google.com/open-source/licenses/bsd +import datetime import os import uuid @@ -448,14 +449,33 @@ def add_index(self, model, index): super().add_index(model, index) def quote_value(self, value): - # A more complete implementation isn't currently required. + if value is None: + return "NULL" if isinstance(value, str): # GoogleSQL string literals use backslash escaping; '' quote # doubling is not recognized, so escape the backslash first and # then the quote (matching the db_default/generated inlining above). - return "'%s'" % value.replace("\\", "\\\\").replace("'", "\\'") + # Literal newlines are not allowed inside the quotes either. + return "'%s'" % ( + value.replace("\\", "\\\\") + .replace("'", "\\'") + .replace("\n", "\\n") + .replace("\r", "\\r") + ) if isinstance(value, bool): return "TRUE" if value else "FALSE" + if isinstance(value, (bytes, bytearray, memoryview)): + # GoogleSQL bytes literal. The quote, the backslash and anything + # outside printable ASCII are emitted as \x escapes. + escaped = "".join( + chr(b) if 0x20 <= b <= 0x7E and b not in (0x27, 0x5C) else "\\x%02x" % b + for b in bytes(value) + ) + return "b'%s'" % escaped + if isinstance(value, datetime.datetime): + return "'%s'" % value.isoformat(sep=" ") + if isinstance(value, datetime.date): + return "'%s'" % value.isoformat() return str(value) def prepare_default(self, value): diff --git a/packages/django-google-spanner/tests/unit/django_spanner/test_schema.py b/packages/django-google-spanner/tests/unit/django_spanner/test_schema.py index 0ce848f9afe3..90fda59cbe7a 100644 --- a/packages/django-google-spanner/tests/unit/django_spanner/test_schema.py +++ b/packages/django-google-spanner/tests/unit/django_spanner/test_schema.py @@ -5,6 +5,7 @@ # https://developers.google.com/open-source/licenses/bsd +import datetime from unittest import mock from django.db import NotSupportedError, connection, connections @@ -53,6 +54,50 @@ def test_quote_value_escapes_string(self): "'\\\\\\'; DROP TABLE t; --'", ) + def test_quote_value_escapes_newlines_and_carriage_returns(self): + schema_editor = DatabaseSchemaEditor(self.connection) + self.assertEqual( + schema_editor.quote_value("line1\nline2"), + "'line1\\nline2'", + ) + self.assertEqual( + schema_editor.quote_value("line1\r\nline2"), + "'line1\\r\\nline2'", + ) + + def test_quote_value_handles_none(self): + schema_editor = DatabaseSchemaEditor(self.connection) + self.assertEqual(schema_editor.quote_value(None), "NULL") + + def test_quote_value_handles_date_and_datetime(self): + schema_editor = DatabaseSchemaEditor(self.connection) + self.assertEqual( + schema_editor.quote_value(datetime.date(2026, 9, 4)), + "'2026-09-04'", + ) + self.assertEqual( + schema_editor.quote_value(datetime.datetime(2026, 9, 4, 12, 0, 0)), + "'2026-09-04 12:00:00'", + ) + + def test_quote_value_handles_bytes(self): + schema_editor = DatabaseSchemaEditor(self.connection) + self.assertEqual(schema_editor.quote_value(b"abc"), "b'abc'") + self.assertEqual( + schema_editor.quote_value(b"\x00'\\\n\xff"), + "b'\\x00\\x27\\x5c\\x0a\\xff'", + ) + + def test_quote_value_booleans(self): + schema_editor = DatabaseSchemaEditor(self.connection) + self.assertEqual(schema_editor.quote_value(True), "TRUE") + self.assertEqual(schema_editor.quote_value(False), "FALSE") + + def test_prepare_default_delegates_to_quote_value(self): + schema_editor = DatabaseSchemaEditor(self.connection) + self.assertEqual(schema_editor.prepare_default("o'brien"), "'o\\'brien'") + self.assertEqual(schema_editor.prepare_default(True), "TRUE") + def test_skip_default(self): """ Tries skipping default as Cloud spanner doesn't support it.