Skip to content

Commit e109b70

Browse files
committed
fix(django-spanner): escape newlines and handle None, bytes and dates in quote_value
1 parent ce44500 commit e109b70

2 files changed

Lines changed: 67 additions & 2 deletions

File tree

packages/django-google-spanner/django_spanner/schema.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# Use of this source code is governed by a BSD-style
44
# license that can be found in the LICENSE file or at
55
# https://developers.google.com/open-source/licenses/bsd
6+
import datetime
67
import os
78
import uuid
89

@@ -448,14 +449,33 @@ def add_index(self, model, index):
448449
super().add_index(model, index)
449450

450451
def quote_value(self, value):
451-
# A more complete implementation isn't currently required.
452+
if value is None:
453+
return "NULL"
452454
if isinstance(value, str):
453455
# GoogleSQL string literals use backslash escaping; '' quote
454456
# doubling is not recognized, so escape the backslash first and
455457
# then the quote (matching the db_default/generated inlining above).
456-
return "'%s'" % value.replace("\\", "\\\\").replace("'", "\\'")
458+
# Literal newlines are not allowed inside the quotes either.
459+
return "'%s'" % (
460+
value.replace("\\", "\\\\")
461+
.replace("'", "\\'")
462+
.replace("\n", "\\n")
463+
.replace("\r", "\\r")
464+
)
457465
if isinstance(value, bool):
458466
return "TRUE" if value else "FALSE"
467+
if isinstance(value, (bytes, bytearray, memoryview)):
468+
# GoogleSQL bytes literal. The quote, the backslash and anything
469+
# outside printable ASCII are emitted as \x escapes.
470+
escaped = "".join(
471+
chr(b) if 0x20 <= b <= 0x7E and b not in (0x27, 0x5C) else "\\x%02x" % b
472+
for b in bytes(value)
473+
)
474+
return "b'%s'" % escaped
475+
if isinstance(value, datetime.datetime):
476+
return "'%s'" % value.isoformat(sep=" ")
477+
if isinstance(value, datetime.date):
478+
return "'%s'" % value.isoformat()
459479
return str(value)
460480

461481
def prepare_default(self, value):

packages/django-google-spanner/tests/unit/django_spanner/test_schema.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
# https://developers.google.com/open-source/licenses/bsd
66

77

8+
import datetime
89
from unittest import mock
910

1011
from django.db import NotSupportedError, connection, connections
@@ -53,6 +54,50 @@ def test_quote_value_escapes_string(self):
5354
"'\\\\\\'; DROP TABLE t; --'",
5455
)
5556

57+
def test_quote_value_escapes_newlines_and_carriage_returns(self):
58+
schema_editor = DatabaseSchemaEditor(self.connection)
59+
self.assertEqual(
60+
schema_editor.quote_value("line1\nline2"),
61+
"'line1\\nline2'",
62+
)
63+
self.assertEqual(
64+
schema_editor.quote_value("line1\r\nline2"),
65+
"'line1\\r\\nline2'",
66+
)
67+
68+
def test_quote_value_handles_none(self):
69+
schema_editor = DatabaseSchemaEditor(self.connection)
70+
self.assertEqual(schema_editor.quote_value(None), "NULL")
71+
72+
def test_quote_value_handles_date_and_datetime(self):
73+
schema_editor = DatabaseSchemaEditor(self.connection)
74+
self.assertEqual(
75+
schema_editor.quote_value(datetime.date(2026, 9, 4)),
76+
"'2026-09-04'",
77+
)
78+
self.assertEqual(
79+
schema_editor.quote_value(datetime.datetime(2026, 9, 4, 12, 0, 0)),
80+
"'2026-09-04 12:00:00'",
81+
)
82+
83+
def test_quote_value_handles_bytes(self):
84+
schema_editor = DatabaseSchemaEditor(self.connection)
85+
self.assertEqual(schema_editor.quote_value(b"abc"), "b'abc'")
86+
self.assertEqual(
87+
schema_editor.quote_value(b"\x00'\\\n\xff"),
88+
"b'\\x00\\x27\\x5c\\x0a\\xff'",
89+
)
90+
91+
def test_quote_value_booleans(self):
92+
schema_editor = DatabaseSchemaEditor(self.connection)
93+
self.assertEqual(schema_editor.quote_value(True), "TRUE")
94+
self.assertEqual(schema_editor.quote_value(False), "FALSE")
95+
96+
def test_prepare_default_delegates_to_quote_value(self):
97+
schema_editor = DatabaseSchemaEditor(self.connection)
98+
self.assertEqual(schema_editor.prepare_default("o'brien"), "'o\\'brien'")
99+
self.assertEqual(schema_editor.prepare_default(True), "TRUE")
100+
56101
def test_skip_default(self):
57102
"""
58103
Tries skipping default as Cloud spanner doesn't support it.

0 commit comments

Comments
 (0)