ddsql 0.0.2__tar.gz → 0.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ddsql
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: SQL Query Library
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -58,6 +58,20 @@ or create your own by inheriting from `BaseSerializer`.
58
58
  | `date` | `'2025-01-01'` | `'2025-01-01'::date` | `toDate('2025-01-01')` |
59
59
  | `list`/`tuple`/`set` | `(item1, item2, ...)` | `(item1, item2, ...)` | `(item1, item2, ...)` |
60
60
 
61
+ String values are escaped according to the dialect rules, so quotes, backslashes
62
+ and control characters cannot break the query:
63
+
64
+ - `BaseSerializer` doubles single quotes (`O'Brien` → `'O''Brien'`), as defined by the SQL standard.
65
+ - `ClickhouseSerializer` escapes backslashes, single quotes and all control characters
66
+ with backslash sequences (`O'Brien` → `'O\'Brien'`, newline → `\n`, other control characters → `\xHH`).
67
+ - `PostgresSerializer` doubles single quotes; strings containing backslashes or control
68
+ characters are emitted using the escape string syntax (`C:\dir` → `E'C:\\dir'`), which is
69
+ interpreted the same way regardless of the `standard_conforming_strings` server setting.
70
+ A NUL (`0x00`) character raises `ValueError`, since PostgreSQL cannot store it in text values.
71
+
72
+ The serialized literal is always a single printable line.
73
+ To customize escaping in your own serializer, override the `escape_string` method.
74
+
61
75
  If you need to serialize a type not listed in the table, override the `serialize_other_object` method in your serializer:
62
76
 
63
77
  ```python
@@ -36,6 +36,20 @@ or create your own by inheriting from `BaseSerializer`.
36
36
  | `date` | `'2025-01-01'` | `'2025-01-01'::date` | `toDate('2025-01-01')` |
37
37
  | `list`/`tuple`/`set` | `(item1, item2, ...)` | `(item1, item2, ...)` | `(item1, item2, ...)` |
38
38
 
39
+ String values are escaped according to the dialect rules, so quotes, backslashes
40
+ and control characters cannot break the query:
41
+
42
+ - `BaseSerializer` doubles single quotes (`O'Brien` → `'O''Brien'`), as defined by the SQL standard.
43
+ - `ClickhouseSerializer` escapes backslashes, single quotes and all control characters
44
+ with backslash sequences (`O'Brien` → `'O\'Brien'`, newline → `\n`, other control characters → `\xHH`).
45
+ - `PostgresSerializer` doubles single quotes; strings containing backslashes or control
46
+ characters are emitted using the escape string syntax (`C:\dir` → `E'C:\\dir'`), which is
47
+ interpreted the same way regardless of the `standard_conforming_strings` server setting.
48
+ A NUL (`0x00`) character raises `ValueError`, since PostgreSQL cannot store it in text values.
49
+
50
+ The serialized literal is always a single printable line.
51
+ To customize escaping in your own serializer, override the `escape_string` method.
52
+
39
53
  If you need to serialize a type not listed in the table, override the `serialize_other_object` method in your serializer:
40
54
 
41
55
  ```python
@@ -45,20 +45,25 @@ class BaseSerializer:
45
45
  return f'{value}'
46
46
 
47
47
  @staticmethod
48
- def serialize_string(value: str) -> str:
49
- return f"'{value}'"
48
+ def escape_string(value: str) -> str:
49
+ # SQL standard escaping: a single quote is escaped by doubling it
50
+ return value.replace("'", "''")
50
51
 
51
- @staticmethod
52
- def serialize_uuid(value: UUID) -> str:
53
- return f"'{value}'"
52
+ @classmethod
53
+ def serialize_string(cls, value: str) -> str:
54
+ return f"'{cls.escape_string(value)}'"
54
55
 
55
- @staticmethod
56
- def serialize_datetime(value: datetime) -> str:
57
- return f"'{value.isoformat()}'"
56
+ @classmethod
57
+ def serialize_uuid(cls, value: UUID) -> str:
58
+ return f"'{cls.escape_string(str(value))}'"
58
59
 
59
- @staticmethod
60
- def serialize_date(value: date) -> str:
61
- return f"'{value.isoformat()}'"
60
+ @classmethod
61
+ def serialize_datetime(cls, value: datetime) -> str:
62
+ return f"'{cls.escape_string(value.isoformat())}'"
63
+
64
+ @classmethod
65
+ def serialize_date(cls, value: date) -> str:
66
+ return f"'{cls.escape_string(value.isoformat())}'"
62
67
 
63
68
  def serialize_collection(self, value: Collection) -> str:
64
69
  items = ', '.join(self.serialize_value(item) for item in value)
@@ -0,0 +1,50 @@
1
+ from datetime import date, datetime
2
+ from typing import Dict
3
+ from uuid import UUID
4
+
5
+ from ddsql.serializers import BaseSerializer
6
+
7
+
8
+ def _build_escape_translation() -> Dict[int, str]:
9
+ # https://clickhouse.com/docs/en/sql-reference/syntax#string
10
+ translation = {
11
+ ord('\\'): '\\\\',
12
+ ord("'"): "\\'",
13
+ ord('\0'): '\\0',
14
+ ord('\a'): '\\a',
15
+ ord('\b'): '\\b',
16
+ ord('\t'): '\\t',
17
+ ord('\n'): '\\n',
18
+ ord('\v'): '\\v',
19
+ ord('\f'): '\\f',
20
+ ord('\r'): '\\r',
21
+ }
22
+ # control characters without a named escape sequence
23
+ for code in range(0x20):
24
+ translation.setdefault(code, f'\\x{code:02X}')
25
+ translation[0x7F] = '\\x7F'
26
+ return translation
27
+
28
+
29
+ _ESCAPE_TRANSLATION = _build_escape_translation()
30
+
31
+
32
+ class ClickhouseSerializer(BaseSerializer):
33
+ @staticmethod
34
+ def escape_string(value: str) -> str:
35
+ return value.translate(_ESCAPE_TRANSLATION)
36
+
37
+ @classmethod
38
+ def serialize_uuid(cls, value: UUID) -> str:
39
+ return f"toUUID('{cls.escape_string(str(value))}')"
40
+
41
+ @classmethod
42
+ def serialize_datetime(cls, value: datetime) -> str:
43
+ return f"parseDateTimeBestEffort('{cls.escape_string(value.isoformat())}')"
44
+
45
+ @classmethod
46
+ def serialize_date(cls, value: date) -> str:
47
+ return f"toDate('{cls.escape_string(value.isoformat())}')"
48
+
49
+
50
+ __all__ = ('ClickhouseSerializer',)
@@ -0,0 +1,62 @@
1
+ import re
2
+ from datetime import date, datetime
3
+ from typing import Dict
4
+ from uuid import UUID
5
+
6
+ from ddsql.serializers import BaseSerializer
7
+
8
+ # Characters a regular literal cannot represent reliably: control characters have no escape
9
+ # sequences there, and a backslash changes meaning when standard_conforming_strings is off.
10
+ # Strings containing them are serialized with the escape string syntax (E'...') instead.
11
+ _ESCAPE_SYNTAX_REQUIRED = re.compile(r'[\\\x00-\x1F\x7F]')
12
+
13
+
14
+ def _build_escape_translation() -> Dict[int, str]:
15
+ # https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS-ESCAPE
16
+ translation = {
17
+ ord('\\'): '\\\\',
18
+ ord("'"): "''",
19
+ ord('\b'): '\\b',
20
+ ord('\t'): '\\t',
21
+ ord('\n'): '\\n',
22
+ ord('\f'): '\\f',
23
+ ord('\r'): '\\r',
24
+ }
25
+ # control characters without a named escape sequence
26
+ for code in range(0x20):
27
+ translation.setdefault(code, f'\\x{code:02X}')
28
+ translation[0x7F] = '\\x7F'
29
+ return translation
30
+
31
+
32
+ _ESCAPE_TRANSLATION = _build_escape_translation()
33
+
34
+
35
+ class PostgresSerializer(BaseSerializer):
36
+ @staticmethod
37
+ def escape_string(value: str) -> str:
38
+ # PostgreSQL follows the SQL standard: a single quote is escaped by doubling it
39
+ return value.replace("'", "''")
40
+
41
+ @classmethod
42
+ def serialize_string(cls, value: str) -> str:
43
+ if '\x00' in value:
44
+ raise ValueError('PostgreSQL does not support the NUL (0x00) character in string values')
45
+ if _ESCAPE_SYNTAX_REQUIRED.search(value):
46
+ return f"E'{value.translate(_ESCAPE_TRANSLATION)}'"
47
+ return f"'{cls.escape_string(value)}'"
48
+
49
+ @classmethod
50
+ def serialize_uuid(cls, value: UUID) -> str:
51
+ return f"'{cls.escape_string(str(value))}'::uuid"
52
+
53
+ @classmethod
54
+ def serialize_datetime(cls, value: datetime) -> str:
55
+ return f"'{cls.escape_string(value.isoformat())}'::timestamp"
56
+
57
+ @classmethod
58
+ def serialize_date(cls, value: date) -> str:
59
+ return f"'{cls.escape_string(value.isoformat())}'::date"
60
+
61
+
62
+ __all__ = ('PostgresSerializer',)
@@ -1,7 +1,7 @@
1
1
  [tool.poetry]
2
2
  name = "ddsql"
3
3
  description = "SQL Query Library"
4
- version = "0.0.2"
4
+ version = "0.0.3"
5
5
  authors = ["davyddd"]
6
6
  license = "MIT"
7
7
  repository = "https://github.com/davyddd/ddsql"
@@ -1,21 +0,0 @@
1
- from datetime import date, datetime
2
- from uuid import UUID
3
-
4
- from ddsql.serializers import BaseSerializer
5
-
6
-
7
- class ClickhouseSerializer(BaseSerializer):
8
- @staticmethod
9
- def serialize_uuid(value: UUID) -> str:
10
- return f"toUUID('{value}')"
11
-
12
- @staticmethod
13
- def serialize_datetime(value: datetime) -> str:
14
- return f"parseDateTimeBestEffort('{value.isoformat()}')"
15
-
16
- @staticmethod
17
- def serialize_date(value: date) -> str:
18
- return f"toDate('{value.isoformat()}')"
19
-
20
-
21
- __all__ = ('ClickhouseSerializer',)
@@ -1,21 +0,0 @@
1
- from datetime import date, datetime
2
- from uuid import UUID
3
-
4
- from ddsql.serializers import BaseSerializer
5
-
6
-
7
- class PostgresSerializer(BaseSerializer):
8
- @staticmethod
9
- def serialize_uuid(value: UUID) -> str:
10
- return f"'{value}'::uuid"
11
-
12
- @staticmethod
13
- def serialize_datetime(value: datetime) -> str:
14
- return f"'{value.isoformat()}'::timestamp"
15
-
16
- @staticmethod
17
- def serialize_date(value: date) -> str:
18
- return f"'{value.isoformat()}'::date"
19
-
20
-
21
- __all__ = ('PostgresSerializer',)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes