diff --git a/.changes/unreleased/Fixed-20260816-140000.yaml b/.changes/unreleased/Fixed-20260816-140000.yaml new file mode 100644 index 00000000..71bb5e42 --- /dev/null +++ b/.changes/unreleased/Fixed-20260816-140000.yaml @@ -0,0 +1,6 @@ +kind: Fixed +body: 'A query containing `"""` no longer produces a module that does not parse. The SQL went into a `"""` literal, so three consecutive double quotes in the query - from a string literal, or from a quoted identifier ending in a quote - closed the constant early and left the rest of the file as stray syntax. The constant and the docstring now pick a delimiter the query does not contain. A query holding both `"""` and `''''''` has no block spelling left: the constant falls back to an escaped one-liner, and the docstring, which cannot be escaped without tripping ruff D301, leaves the SQL out.' +time: 2026-08-16T14:00:00.0000000Z +custom: + Author: Rayakame + PR: "263" diff --git a/internal/render/queries.go b/internal/render/queries.go index a79fe8af..e6d1e0c1 100644 --- a/internal/render/queries.go +++ b/internal/render/queries.go @@ -72,18 +72,18 @@ func (r *Renderer) renderQueriesModule(moduleName string, queries []model.Query) constType = "typing.LiteralString" } for _, query := range queries { - constantsBody.WriteLine( - fmt.Sprintf( - `%s: typing.Final[%s] = %s"""-- name: %s %s`, - query.ConstantName, - constType, - writer.PyRawPrefix(query.SQL), - query.QueryName, - query.Cmd, - ), - ) - constantsBody.WriteLine(query.SQL) - constantsBody.WriteLine(`"""`) + assignment := fmt.Sprintf(`%s: typing.Final[%s] = `, query.ConstantName, constType) + header := fmt.Sprintf("-- name: %s %s", query.QueryName, query.Cmd) + if literal, ok := writer.PyTripleQuotedFor(query.SQL); ok { + constantsBody.WriteLine(assignment + literal.Prefix + literal.Delimiter + header) + constantsBody.WriteLine(query.SQL) + constantsBody.WriteLine(literal.Delimiter) + } else { + // SQL holding both triple quotes has no block spelling left. An + // escaped one-liner still carries it exactly; only the constant's + // resemblance to the query is lost. + constantsBody.WriteLine(assignment + writer.PyQuote(header+"\n"+query.SQL+"\n")) + } constantsBody.NewLine() if query.Returns.EmitTable { diff --git a/internal/render/render_queries_test.go b/internal/render/render_queries_test.go index ec4fc046..2b0e25fe 100644 --- a/internal/render/render_queries_test.go +++ b/internal/render/render_queries_test.go @@ -80,6 +80,73 @@ UPDATE test_items SET path = 'C:\dir', tag = 'a\tb' WHERE path ~ '\d+' AND id = """ +async def tag_item(conn: ConnectionLike, *, id_: int) -> None: + await conn.execute(TAG_ITEM, id_) +`, + }, + { + // The literal cannot be delimited by a quote run it contains. + name: "a triple double quote switches the delimiter", + engine: "postgresql", + options: `{"package":"testpkg","sql_driver":"asyncpg","emit_init_file":false}`, + queries: []*plugin.Query{{ + Name: "TagItem", + Cmd: metadata.CmdExec, + Text: `UPDATE test_items SET tag = 'a"""b' WHERE id = $1`, + Filename: "queries.sql", + Params: []*plugin.Parameter{pgParam(pgColumn("id", "int4", true))}, + }}, + want: sqlcFileHeader("queries.sql") + `from __future__ import annotations + +__all__: collections.abc.Sequence[str] = ("tag_item",) + +import typing + +if typing.TYPE_CHECKING: + import asyncpg + import collections.abc + + type ConnectionLike = asyncpg.Connection[asyncpg.Record] | asyncpg.pool.PoolConnectionProxy[asyncpg.Record] + + +TAG_ITEM: typing.Final[str] = '''-- name: TagItem :exec +UPDATE test_items SET tag = 'a"""b' WHERE id = $1 +''' + + +async def tag_item(conn: ConnectionLike, *, id_: int) -> None: + await conn.execute(TAG_ITEM, id_) +`, + }, + { + // Holding both quote runs leaves no block spelling, so the + // constant falls back to an escaped one-liner. + name: "both triple quotes fall back to an escaped literal", + engine: "postgresql", + options: `{"package":"testpkg","sql_driver":"asyncpg","emit_init_file":false}`, + queries: []*plugin.Query{{ + Name: "TagItem", + Cmd: metadata.CmdExec, + Text: `UPDATE test_items SET tag = 'a"""b' || 'c''''''d' WHERE id = $1`, + Filename: "queries.sql", + Params: []*plugin.Parameter{pgParam(pgColumn("id", "int4", true))}, + }}, + want: sqlcFileHeader("queries.sql") + `from __future__ import annotations + +__all__: collections.abc.Sequence[str] = ("tag_item",) + +import typing + +if typing.TYPE_CHECKING: + import asyncpg + import collections.abc + + type ConnectionLike = asyncpg.Connection[asyncpg.Record] | asyncpg.pool.PoolConnectionProxy[asyncpg.Record] + + +TAG_ITEM: typing.Final[str] = "-- name: TagItem :exec\nUPDATE test_items SET tag = 'a\"\"\"b' || 'c''''''d' WHERE id = $1\n" + + async def tag_item(conn: ConnectionLike, *, id_: int) -> None: await conn.execute(TAG_ITEM, id_) `, diff --git a/internal/writer/docstrings.go b/internal/writer/docstrings.go index 1d9c994d..fbdc3c85 100644 --- a/internal/writer/docstrings.go +++ b/internal/writer/docstrings.go @@ -461,14 +461,17 @@ func (w *CodeWriter) WriteQueryFunctionDocstring(lvl int, query *model.Query, co return } - // Only the embedded SQL can carry a backslash - the rest of the docstring - // is generated prose - and the prefix has to be written before it. - writeSQL := emitSQL && !w.docstringOmitSQL - rawPrefix := "" - if writeSQL { - rawPrefix = PyRawPrefix(query.SQL) - } - w.WriteIndentedLine(lvl, rawPrefix+`"""`+fmt.Sprintf(summaryFmt, query.QueryName, query.Cmd)) + // Only the embedded SQL can carry a backslash or a triple quote - the rest + // of the docstring is generated prose - and how the docstring opens + // depends on both, so it is settled before the summary is written. SQL + // that no docstring can spell is left out of it; the constant still + // carries the query. + literal, canEmbed := PyTripleQuotedFor(query.SQL) + writeSQL := emitSQL && !w.docstringOmitSQL && canEmbed + if !writeSQL { + literal = PyTripleQuoted{Prefix: "", Delimiter: pyTripleQuote} + } + w.WriteIndentedLine(lvl, literal.Prefix+literal.Delimiter+fmt.Sprintf(summaryFmt, query.QueryName, query.Cmd)) w.NewLine() if writeSQL { w.WriteIndentedLine(lvl, "```sql") @@ -491,7 +494,7 @@ func (w *CodeWriter) WriteQueryFunctionDocstring(lvl int, query *model.Query, co if wroteArgs && w.docstringConvention == config.DocstringConventionNumpy { w.NewLine() } - w.WriteIndentedLine(lvl, `"""`) + w.WriteIndentedLine(lvl, literal.Delimiter) return } @@ -500,7 +503,7 @@ func (w *CodeWriter) WriteQueryFunctionDocstring(lvl int, query *model.Query, co w.NewLine() } w.writeDocReturnsSection(lvl, ret) - w.WriteIndentedLine(lvl, `"""`) + w.WriteIndentedLine(lvl, literal.Delimiter) } // writeDocArgsSection writes the Parameters/Args/Arguments section and reports diff --git a/internal/writer/docstrings_test.go b/internal/writer/docstrings_test.go index 07e2410c..23b28720 100644 --- a/internal/writer/docstrings_test.go +++ b/internal/writer/docstrings_test.go @@ -735,6 +735,39 @@ func TestWriteQueryFunctionDocstring(t *testing.T) { ` """`, ), }, + { + // The docstring cannot be delimited by a quote run the SQL holds. + name: "a triple double quote switches the delimiter", + conv: config.DocstringConventionGoogle, + write: func(w *writer.CodeWriter) { + query := &model.Query{Cmd: metadata.CmdExec, QueryName: "Tag", SQL: `UPDATE t SET tag = 'a"""b'`} + w.WriteQueryFunctionDocstring(1, query, "", nil, "") + }, + want: lines( + " '''Execute SQL query with `name: Tag :exec`.", + ``, + " ```sql", + ` UPDATE t SET tag = 'a"""b'`, + " ```", + ``, + ` '''`, + ), + }, + { + // Holding both quote runs leaves no docstring spelling at all, and + // escaping is not open to one. The constant still carries the SQL. + name: "both triple quotes drop the sql block", + conv: config.DocstringConventionGoogle, + write: func(w *writer.CodeWriter) { + query := &model.Query{Cmd: metadata.CmdExec, QueryName: "Tag", SQL: `UPDATE t SET tag = 'a"""b' || 'c''''''d'`} + w.WriteQueryFunctionDocstring(1, query, "", nil, "") + }, + want: lines( + " \"\"\"Execute SQL query with `name: Tag :exec`.", + ``, + ` """`, + ), + }, { // Without the SQL block nothing in the docstring can hold one. name: "omitted sql needs no raw prefix", diff --git a/internal/writer/writer.go b/internal/writer/writer.go index be26333f..e7e4c74f 100644 --- a/internal/writer/writer.go +++ b/internal/writer/writer.go @@ -160,22 +160,47 @@ func (w *CodeWriter) indent(level int) string { return strings.Repeat(w.indentChar, level*w.charsPerIndentLevel) } -// PyRawPrefix returns the prefix a triple-quoted Python literal needs to carry -// text through verbatim: "r" when the text holds a backslash, "" otherwise. +// PyTripleQuoted is how to spell a triple-quoted Python literal that carries a +// block of text verbatim: the prefix and the delimiter that opens and closes +// it. +type PyTripleQuoted struct { + // Prefix is "r" when the text holds a backslash, "" otherwise. A plain + // literal reads "\t" as a tab and "\n" as a newline, so SQL carrying + // either reaches the server changed, and an escape Python does not know + // ("\d") is a SyntaxWarning today and a SyntaxError in a later version. + // Escaping is not an option for docstrings - ruff D301 wants the raw + // prefix whatever the backslash spells - so both emitters use this rule. + Prefix string + // Delimiter is the triple quote the text does not contain itself. + Delimiter string +} + +// pyTripleQuote is what a triple-quoted literal is delimited with unless its +// own text contains it; pyTripleQuotes lists both spellings in preference +// order. +const pyTripleQuote = `"""` + +var pyTripleQuotes = []string{pyTripleQuote, `'''`} //nolint:gochecknoglobals + +// PyTripleQuotedFor returns how to spell a literal holding text, and false +// when no triple-quoted literal can hold it - text containing both delimiters +// can only be escaped, which a docstring cannot be. // -// A plain literal reads "\t" as a tab and "\n" as a newline, so SQL carrying -// either reaches the server changed, and an escape Python does not know ("\d") -// is a SyntaxWarning today and a SyntaxError in a later version. Escaping is -// not an option for docstrings - ruff D301 wants the raw prefix whatever the -// backslash spells - so both emitters use this one rule. It is only safe -// because they put the closing delimiter on its own line: a raw literal cannot -// end in a backslash. -func PyRawPrefix(text string) string { +// The spelling is only safe because callers put the closing delimiter on its +// own line: a raw literal cannot end in a backslash, and a quote right before +// the delimiter would extend it. +func PyTripleQuotedFor(text string) (PyTripleQuoted, bool) { + prefix := "" if strings.ContainsRune(text, '\\') { - return "r" + prefix = "r" + } + for _, delimiter := range pyTripleQuotes { + if !strings.Contains(text, delimiter) { + return PyTripleQuoted{Prefix: prefix, Delimiter: delimiter}, true + } } - return "" + return PyTripleQuoted{Prefix: "", Delimiter: ""}, false } // PyQuote returns a complete Python string literal. Go's Quote escaping is a diff --git a/internal/writer/writer_test.go b/internal/writer/writer_test.go index 5d5d2d1a..86e1c107 100644 --- a/internal/writer/writer_test.go +++ b/internal/writer/writer_test.go @@ -243,24 +243,74 @@ func TestWriteWrappedCall(t *testing.T) { } } -func TestPyRawPrefix(t *testing.T) { +func TestPyTripleQuotedFor(t *testing.T) { t.Parallel() cases := []struct { - name string - in string - want string + name string + in string + wantOK bool + want writer.PyTripleQuoted }{ - {name: "empty text", in: "", want: ""}, - {name: "no backslash", in: "SELECT 1", want: ""}, - {name: "escape python knows", in: `LIKE 'a\tb'`, want: "r"}, - {name: "escape python does not know", in: `REGEXP '\d+'`, want: "r"}, - {name: "doubled backslash", in: `LIKE 'C:\\n%'`, want: "r"}, + {name: "empty text", in: "", wantOK: true, want: writer.PyTripleQuoted{Prefix: "", Delimiter: `"""`}}, + {name: "plain text", in: "SELECT 1", wantOK: true, want: writer.PyTripleQuoted{Prefix: "", Delimiter: `"""`}}, + { + name: "escape python knows", + in: `LIKE 'a\tb'`, + wantOK: true, + want: writer.PyTripleQuoted{Prefix: "r", Delimiter: `"""`}, + }, + { + name: "escape python does not know", + in: `REGEXP '\d+'`, + wantOK: true, + want: writer.PyTripleQuoted{Prefix: "r", Delimiter: `"""`}, + }, + { + name: "doubled backslash", + in: `LIKE 'C:\\n%'`, + wantOK: true, + want: writer.PyTripleQuoted{Prefix: "r", Delimiter: `"""`}, + }, + { + name: "two double quotes still fit", + in: `= 'a""b'`, + wantOK: true, + want: writer.PyTripleQuoted{Prefix: "", Delimiter: `"""`}, + }, + { + name: "triple double quote falls back", + in: `= 'a"""b'`, + wantOK: true, + want: writer.PyTripleQuoted{Prefix: "", Delimiter: "'''"}, + }, + { + name: "triple single quote keeps double", + in: `= "a'''b"`, + wantOK: true, + want: writer.PyTripleQuoted{Prefix: "", Delimiter: `"""`}, + }, + { + name: "backslash with a triple double quote", + in: `= 'a"""b\d'`, + wantOK: true, + want: writer.PyTripleQuoted{Prefix: "r", Delimiter: "'''"}, + }, + { + name: "both delimiters have no spelling", + in: `= 'a"""b' || 'c''''''d'`, + wantOK: false, + want: writer.PyTripleQuoted{Prefix: "", Delimiter: ""}, + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() - if got := writer.PyRawPrefix(tc.in); got != tc.want { - t.Errorf("PyRawPrefix(%q) = %q, want %q", tc.in, got, tc.want) + got, ok := writer.PyTripleQuotedFor(tc.in) + if ok != tc.wantOK { + t.Fatalf("PyTripleQuotedFor(%q) ok = %v, want %v", tc.in, ok, tc.wantOK) + } + if got != tc.want { + t.Errorf("PyTripleQuotedFor(%q) = %+v, want %+v", tc.in, got, tc.want) } }) } diff --git a/sqlc.yaml b/sqlc.yaml index 7595a3ed..714cfd23 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -3,7 +3,7 @@ plugins: - name: python wasm: url: file://sqlc-gen-better-python.wasm - sha256: 2bad9ba936a60fc014eb4503a300a3ebfcd79483bd215002a8d4d03f1b290201 + sha256: ea40a229077a701acb2d5282e95d8dd7cf9759ca8fe2558fe7b2fe0062f63627 sql: - schema: test/schema.sql queries: test/queries.sql diff --git a/test/driver_aiosqlite/sqlc-gen-better-python.wasm b/test/driver_aiosqlite/sqlc-gen-better-python.wasm index cdd42a7d..1d453384 100644 Binary files a/test/driver_aiosqlite/sqlc-gen-better-python.wasm and b/test/driver_aiosqlite/sqlc-gen-better-python.wasm differ diff --git a/test/driver_aiosqlite/sqlc.yaml b/test/driver_aiosqlite/sqlc.yaml index bfe3f36b..12f25270 100644 --- a/test/driver_aiosqlite/sqlc.yaml +++ b/test/driver_aiosqlite/sqlc.yaml @@ -3,7 +3,7 @@ plugins: - name: python wasm: url: file://sqlc-gen-better-python.wasm - sha256: 2bad9ba936a60fc014eb4503a300a3ebfcd79483bd215002a8d4d03f1b290201 + sha256: ea40a229077a701acb2d5282e95d8dd7cf9759ca8fe2558fe7b2fe0062f63627 sql: - schema: schema.sql queries: queries.sql diff --git a/test/driver_asyncmy/sqlc-gen-better-python.wasm b/test/driver_asyncmy/sqlc-gen-better-python.wasm index cdd42a7d..1d453384 100755 Binary files a/test/driver_asyncmy/sqlc-gen-better-python.wasm and b/test/driver_asyncmy/sqlc-gen-better-python.wasm differ diff --git a/test/driver_asyncmy/sqlc.yaml b/test/driver_asyncmy/sqlc.yaml index 413c4cd5..34b4edd8 100644 --- a/test/driver_asyncmy/sqlc.yaml +++ b/test/driver_asyncmy/sqlc.yaml @@ -3,7 +3,7 @@ plugins: - name: python wasm: url: file://sqlc-gen-better-python.wasm - sha256: 2bad9ba936a60fc014eb4503a300a3ebfcd79483bd215002a8d4d03f1b290201 + sha256: ea40a229077a701acb2d5282e95d8dd7cf9759ca8fe2558fe7b2fe0062f63627 sql: - schema: schema.sql queries: diff --git a/test/driver_asyncpg/sqlc-gen-better-python.wasm b/test/driver_asyncpg/sqlc-gen-better-python.wasm index cdd42a7d..1d453384 100644 Binary files a/test/driver_asyncpg/sqlc-gen-better-python.wasm and b/test/driver_asyncpg/sqlc-gen-better-python.wasm differ diff --git a/test/driver_asyncpg/sqlc.yaml b/test/driver_asyncpg/sqlc.yaml index 440dcee3..c981e8cc 100644 --- a/test/driver_asyncpg/sqlc.yaml +++ b/test/driver_asyncpg/sqlc.yaml @@ -3,7 +3,7 @@ plugins: - name: python wasm: url: file://sqlc-gen-better-python.wasm - sha256: 2bad9ba936a60fc014eb4503a300a3ebfcd79483bd215002a8d4d03f1b290201 + sha256: ea40a229077a701acb2d5282e95d8dd7cf9759ca8fe2558fe7b2fe0062f63627 sql: - schema: schema.sql queries: diff --git a/test/driver_psycopg_async/sqlc-gen-better-python.wasm b/test/driver_psycopg_async/sqlc-gen-better-python.wasm index cdd42a7d..1d453384 100644 Binary files a/test/driver_psycopg_async/sqlc-gen-better-python.wasm and b/test/driver_psycopg_async/sqlc-gen-better-python.wasm differ diff --git a/test/driver_psycopg_async/sqlc.yaml b/test/driver_psycopg_async/sqlc.yaml index 159e669d..4ee18a0e 100644 --- a/test/driver_psycopg_async/sqlc.yaml +++ b/test/driver_psycopg_async/sqlc.yaml @@ -3,7 +3,7 @@ plugins: - name: python wasm: url: file://sqlc-gen-better-python.wasm - sha256: 2bad9ba936a60fc014eb4503a300a3ebfcd79483bd215002a8d4d03f1b290201 + sha256: ea40a229077a701acb2d5282e95d8dd7cf9759ca8fe2558fe7b2fe0062f63627 sql: - schema: schema.sql queries: diff --git a/test/driver_psycopg_sync/sqlc-gen-better-python.wasm b/test/driver_psycopg_sync/sqlc-gen-better-python.wasm index cdd42a7d..1d453384 100755 Binary files a/test/driver_psycopg_sync/sqlc-gen-better-python.wasm and b/test/driver_psycopg_sync/sqlc-gen-better-python.wasm differ diff --git a/test/driver_psycopg_sync/sqlc.yaml b/test/driver_psycopg_sync/sqlc.yaml index 5db84dee..92fcd09c 100644 --- a/test/driver_psycopg_sync/sqlc.yaml +++ b/test/driver_psycopg_sync/sqlc.yaml @@ -3,7 +3,7 @@ plugins: - name: python wasm: url: file://sqlc-gen-better-python.wasm - sha256: 2bad9ba936a60fc014eb4503a300a3ebfcd79483bd215002a8d4d03f1b290201 + sha256: ea40a229077a701acb2d5282e95d8dd7cf9759ca8fe2558fe7b2fe0062f63627 sql: - schema: schema.sql queries: diff --git a/test/driver_pymysql/sqlc-gen-better-python.wasm b/test/driver_pymysql/sqlc-gen-better-python.wasm index cdd42a7d..1d453384 100755 Binary files a/test/driver_pymysql/sqlc-gen-better-python.wasm and b/test/driver_pymysql/sqlc-gen-better-python.wasm differ diff --git a/test/driver_pymysql/sqlc.yaml b/test/driver_pymysql/sqlc.yaml index 599ba22a..f10dbb5c 100644 --- a/test/driver_pymysql/sqlc.yaml +++ b/test/driver_pymysql/sqlc.yaml @@ -3,7 +3,7 @@ plugins: - name: python wasm: url: file://sqlc-gen-better-python.wasm - sha256: 2bad9ba936a60fc014eb4503a300a3ebfcd79483bd215002a8d4d03f1b290201 + sha256: ea40a229077a701acb2d5282e95d8dd7cf9759ca8fe2558fe7b2fe0062f63627 sql: - schema: schema.sql queries: diff --git a/test/driver_sqlite3/dataclass/functions/queries_triple_quote.py b/test/driver_sqlite3/dataclass/functions/queries_triple_quote.py new file mode 100644 index 00000000..3b437e6d --- /dev/null +++ b/test/driver_sqlite3/dataclass/functions/queries_triple_quote.py @@ -0,0 +1,66 @@ +# Code generated by sqlc. DO NOT EDIT. +# versions: +# sqlc v1.31.1 +# sqlc-gen-better-python v0.8.0 +# source file: queries_triple_quote.sql +"""Module containing queries from file queries_triple_quote.sql.""" + +from __future__ import annotations + +__all__: collections.abc.Sequence[str] = ( + "get_both_quotes_pattern", + "get_triple_quote_pattern", +) + +import typing + +if typing.TYPE_CHECKING: + import collections.abc + import sqlite3 + + +GET_TRIPLE_QUOTE_PATTERN: typing.Final[str] = '''-- name: GetTripleQuotePattern :one + +SELECT 'a"""b' AS pattern FROM test_slice WHERE id = ? +''' + +GET_BOTH_QUOTES_PATTERN: typing.Final[str] = "-- name: GetBothQuotesPattern :one\nSELECT 'a\"\"\"b''''''c' AS pattern FROM test_slice WHERE id = ?\n" + + +def get_triple_quote_pattern(conn: sqlite3.Connection, *, id_: int) -> str | None: + '''Fetch one from the db using the SQL query with `name: GetTripleQuotePattern :one`. + + ```sql + + SELECT 'a"""b' AS pattern FROM test_slice WHERE id = ? + ``` + + Args: + conn: + Connection object of type `sqlite3.Connection` used to execute the query. + id_: int. + + Returns: + Result of type `str` fetched from the db. Will be `None` if not found. + ''' + row = conn.execute(GET_TRIPLE_QUOTE_PATTERN, (id_,)).fetchone() + if row is None: + return None + return row[0] + + +def get_both_quotes_pattern(conn: sqlite3.Connection, *, id_: int) -> str | None: + """Fetch one from the db using the SQL query with `name: GetBothQuotesPattern :one`. + + Args: + conn: + Connection object of type `sqlite3.Connection` used to execute the query. + id_: int. + + Returns: + Result of type `str` fetched from the db. Will be `None` if not found. + """ + row = conn.execute(GET_BOTH_QUOTES_PATTERN, (id_,)).fetchone() + if row is None: + return None + return row[0] diff --git a/test/driver_sqlite3/dataclass/test_sqlite3_dataclass_functions.py b/test/driver_sqlite3/dataclass/test_sqlite3_dataclass_functions.py index 80ad9b7b..6b3c30de 100644 --- a/test/driver_sqlite3/dataclass/test_sqlite3_dataclass_functions.py +++ b/test/driver_sqlite3/dataclass/test_sqlite3_dataclass_functions.py @@ -39,6 +39,7 @@ from test.driver_sqlite3.dataclass.functions import queries_override_adapter from test.driver_sqlite3.dataclass.functions import queries_override_converter from test.driver_sqlite3.dataclass.functions import queries_slice +from test.driver_sqlite3.dataclass.functions import queries_triple_quote from test.driver_sqlite3.dataclass.functions import queries_unknown_override OVERRIDE_PRICE = 12.5 @@ -50,6 +51,7 @@ ANY_PARAM_ID = 565656 SLICE_ID_BASE = 585858 BACKSLASH_ID = 606060 +TRIPLE_QUOTE_ID = 616161 SLICE_ROW_COUNT = 4 @@ -1204,3 +1206,14 @@ def test_backslash_sql(self, sqlite3_conn: sqlite3.Connection) -> None: assert queries_backslash.get_backslash_note(conn=sqlite3_conn, id_=BACKSLASH_ID) == "C:\\dir\\name" assert queries_backslash.get_backslash_pattern(conn=sqlite3_conn, id_=-1) is None assert queries_backslash.get_backslash_note(conn=sqlite3_conn, id_=-1) is None + + @pytest.mark.dependency(name="Sqlite3TestDataclassFunctions::triple_quote_sql") + def test_triple_quote_sql(self, sqlite3_conn: sqlite3.Connection) -> None: + # A Python literal cannot be delimited by a quote run its own text + # holds. The first query forces the other delimiter; the second holds + # both runs and can only be spelled escaped. + queries_backslash.insert_backslash_row(conn=sqlite3_conn, id_=TRIPLE_QUOTE_ID, name="quotes", note=None) + assert queries_triple_quote.get_triple_quote_pattern(conn=sqlite3_conn, id_=TRIPLE_QUOTE_ID) == 'a"""b' + assert queries_triple_quote.get_both_quotes_pattern(conn=sqlite3_conn, id_=TRIPLE_QUOTE_ID) == "a\"\"\"b'''c" + assert queries_triple_quote.get_triple_quote_pattern(conn=sqlite3_conn, id_=-1) is None + assert queries_triple_quote.get_both_quotes_pattern(conn=sqlite3_conn, id_=-1) is None diff --git a/test/driver_sqlite3/queries_triple_quote.sql b/test/driver_sqlite3/queries_triple_quote.sql new file mode 100644 index 00000000..1254a0ff --- /dev/null +++ b/test/driver_sqlite3/queries_triple_quote.sql @@ -0,0 +1,9 @@ +-- The SQL goes into a Python literal, which cannot be delimited by a quote run +-- the query itself contains. The first query forces the other delimiter; the +-- second holds both and leaves no block spelling at all. + +-- name: GetTripleQuotePattern :one +SELECT 'a"""b' AS pattern FROM test_slice WHERE id = ?; + +-- name: GetBothQuotesPattern :one +SELECT 'a"""b''''''c' AS pattern FROM test_slice WHERE id = ?; diff --git a/test/driver_sqlite3/sqlc-gen-better-python.wasm b/test/driver_sqlite3/sqlc-gen-better-python.wasm index cdd42a7d..1d453384 100644 Binary files a/test/driver_sqlite3/sqlc-gen-better-python.wasm and b/test/driver_sqlite3/sqlc-gen-better-python.wasm differ diff --git a/test/driver_sqlite3/sqlc.yaml b/test/driver_sqlite3/sqlc.yaml index d2c4882b..501b9369 100644 --- a/test/driver_sqlite3/sqlc.yaml +++ b/test/driver_sqlite3/sqlc.yaml @@ -3,7 +3,7 @@ plugins: - name: python wasm: url: file://sqlc-gen-better-python.wasm - sha256: 2bad9ba936a60fc014eb4503a300a3ebfcd79483bd215002a8d4d03f1b290201 + sha256: ea40a229077a701acb2d5282e95d8dd7cf9759ca8fe2558fe7b2fe0062f63627 sql: - schema: schema.sql queries: @@ -110,6 +110,7 @@ sql: - queries_slice.sql - queries_named_slice.sql - queries_backslash.sql + - queries_triple_quote.sql engine: sqlite codegen: - out: /dataclass/functions diff --git a/test/driver_turso_async/sqlc-gen-better-python.wasm b/test/driver_turso_async/sqlc-gen-better-python.wasm index cdd42a7d..1d453384 100755 Binary files a/test/driver_turso_async/sqlc-gen-better-python.wasm and b/test/driver_turso_async/sqlc-gen-better-python.wasm differ diff --git a/test/driver_turso_async/sqlc.yaml b/test/driver_turso_async/sqlc.yaml index e2933a48..2036e2a9 100644 --- a/test/driver_turso_async/sqlc.yaml +++ b/test/driver_turso_async/sqlc.yaml @@ -3,7 +3,7 @@ plugins: - name: python wasm: url: file://sqlc-gen-better-python.wasm - sha256: 2bad9ba936a60fc014eb4503a300a3ebfcd79483bd215002a8d4d03f1b290201 + sha256: ea40a229077a701acb2d5282e95d8dd7cf9759ca8fe2558fe7b2fe0062f63627 sql: - schema: schema.sql queries: queries.sql diff --git a/test/driver_turso_sync/sqlc-gen-better-python.wasm b/test/driver_turso_sync/sqlc-gen-better-python.wasm index cdd42a7d..1d453384 100755 Binary files a/test/driver_turso_sync/sqlc-gen-better-python.wasm and b/test/driver_turso_sync/sqlc-gen-better-python.wasm differ diff --git a/test/driver_turso_sync/sqlc.yaml b/test/driver_turso_sync/sqlc.yaml index ef432390..684ee471 100644 --- a/test/driver_turso_sync/sqlc.yaml +++ b/test/driver_turso_sync/sqlc.yaml @@ -3,7 +3,7 @@ plugins: - name: python wasm: url: file://sqlc-gen-better-python.wasm - sha256: 2bad9ba936a60fc014eb4503a300a3ebfcd79483bd215002a8d4d03f1b290201 + sha256: ea40a229077a701acb2d5282e95d8dd7cf9759ca8fe2558fe7b2fe0062f63627 sql: - schema: schema.sql queries: