fakesnow 0.9.21__tar.gz → 0.9.22__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.
Files changed (39) hide show
  1. {fakesnow-0.9.21 → fakesnow-0.9.22}/PKG-INFO +2 -2
  2. {fakesnow-0.9.21 → fakesnow-0.9.22}/fakesnow/fakes.py +12 -9
  3. {fakesnow-0.9.21 → fakesnow-0.9.22}/fakesnow/info_schema.py +1 -0
  4. {fakesnow-0.9.21 → fakesnow-0.9.22}/fakesnow/server.py +1 -0
  5. {fakesnow-0.9.21 → fakesnow-0.9.22}/fakesnow/transforms.py +30 -7
  6. {fakesnow-0.9.21 → fakesnow-0.9.22}/fakesnow.egg-info/PKG-INFO +2 -2
  7. {fakesnow-0.9.21 → fakesnow-0.9.22}/fakesnow.egg-info/requires.txt +1 -1
  8. {fakesnow-0.9.21 → fakesnow-0.9.22}/pyproject.toml +5 -2
  9. {fakesnow-0.9.21 → fakesnow-0.9.22}/tests/test_fakes.py +51 -4
  10. {fakesnow-0.9.21 → fakesnow-0.9.22}/tests/test_info_schema.py +37 -3
  11. {fakesnow-0.9.21 → fakesnow-0.9.22}/tests/test_server.py +26 -12
  12. {fakesnow-0.9.21 → fakesnow-0.9.22}/LICENSE +0 -0
  13. {fakesnow-0.9.21 → fakesnow-0.9.22}/README.md +0 -0
  14. {fakesnow-0.9.21 → fakesnow-0.9.22}/fakesnow/__init__.py +0 -0
  15. {fakesnow-0.9.21 → fakesnow-0.9.22}/fakesnow/__main__.py +0 -0
  16. {fakesnow-0.9.21 → fakesnow-0.9.22}/fakesnow/arrow.py +0 -0
  17. {fakesnow-0.9.21 → fakesnow-0.9.22}/fakesnow/checks.py +0 -0
  18. {fakesnow-0.9.21 → fakesnow-0.9.22}/fakesnow/cli.py +0 -0
  19. {fakesnow-0.9.21 → fakesnow-0.9.22}/fakesnow/expr.py +0 -0
  20. {fakesnow-0.9.21 → fakesnow-0.9.22}/fakesnow/fixtures.py +0 -0
  21. {fakesnow-0.9.21 → fakesnow-0.9.22}/fakesnow/instance.py +0 -0
  22. {fakesnow-0.9.21 → fakesnow-0.9.22}/fakesnow/macros.py +0 -0
  23. {fakesnow-0.9.21 → fakesnow-0.9.22}/fakesnow/py.typed +0 -0
  24. {fakesnow-0.9.21 → fakesnow-0.9.22}/fakesnow/variables.py +0 -0
  25. {fakesnow-0.9.21 → fakesnow-0.9.22}/fakesnow.egg-info/SOURCES.txt +0 -0
  26. {fakesnow-0.9.21 → fakesnow-0.9.22}/fakesnow.egg-info/dependency_links.txt +0 -0
  27. {fakesnow-0.9.21 → fakesnow-0.9.22}/fakesnow.egg-info/entry_points.txt +0 -0
  28. {fakesnow-0.9.21 → fakesnow-0.9.22}/fakesnow.egg-info/top_level.txt +0 -0
  29. {fakesnow-0.9.21 → fakesnow-0.9.22}/setup.cfg +0 -0
  30. {fakesnow-0.9.21 → fakesnow-0.9.22}/tests/test_arrow.py +0 -0
  31. {fakesnow-0.9.21 → fakesnow-0.9.22}/tests/test_checks.py +0 -0
  32. {fakesnow-0.9.21 → fakesnow-0.9.22}/tests/test_cli.py +0 -0
  33. {fakesnow-0.9.21 → fakesnow-0.9.22}/tests/test_connect.py +0 -0
  34. {fakesnow-0.9.21 → fakesnow-0.9.22}/tests/test_expr.py +0 -0
  35. {fakesnow-0.9.21 → fakesnow-0.9.22}/tests/test_patch.py +0 -0
  36. {fakesnow-0.9.21 → fakesnow-0.9.22}/tests/test_sqlalchemy.py +0 -0
  37. {fakesnow-0.9.21 → fakesnow-0.9.22}/tests/test_transforms.py +0 -0
  38. {fakesnow-0.9.21 → fakesnow-0.9.22}/tests/test_users.py +0 -0
  39. {fakesnow-0.9.21 → fakesnow-0.9.22}/tests/test_write_pandas.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fakesnow
3
- Version: 0.9.21
3
+ Version: 0.9.22
4
4
  Summary: Fake Snowflake Connector for Python. Run, mock and test Snowflake DB locally.
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -213,7 +213,7 @@ License-File: LICENSE
213
213
  Requires-Dist: duckdb~=1.0.0
214
214
  Requires-Dist: pyarrow
215
215
  Requires-Dist: snowflake-connector-python
216
- Requires-Dist: sqlglot~=25.5.1
216
+ Requires-Dist: sqlglot~=25.9.0
217
217
  Provides-Extra: dev
218
218
  Requires-Dist: build~=1.0; extra == "dev"
219
219
  Requires-Dist: pandas-stubs; extra == "dev"
@@ -114,7 +114,6 @@ class FakeSnowflakeCursor:
114
114
  def description(self) -> list[ResultMetadata]:
115
115
  # use a separate cursor to avoid consuming the result set on this cursor
116
116
  with self._conn.cursor() as cur:
117
- # self._duck_conn.execute(sql, params)
118
117
  expression = sqlglot.parse_one(f"DESCRIBE {self._last_sql}", read="duckdb")
119
118
  cur._execute(expression, self._last_params) # noqa: SLF001
120
119
  meta = FakeSnowflakeCursor._describe_as_result_metadata(cur.fetchall())
@@ -235,12 +234,10 @@ class FakeSnowflakeCursor:
235
234
  if transformed.find(exp.Select) and (seed := transformed.args.get("seed")):
236
235
  sql = f"SELECT setseed({seed}); {sql}"
237
236
 
238
- if (fs_debug := os.environ.get("FAKESNOW_DEBUG")) and fs_debug != "snowflake":
239
- print(f"{sql};{params=}" if params else f"{sql};", file=sys.stderr)
240
-
241
237
  result_sql = None
242
238
 
243
239
  try:
240
+ self._log_sql(sql, params)
244
241
  self._duck_conn.execute(sql, params)
245
242
  except duckdb.BinderException as e:
246
243
  msg = e.args[0]
@@ -287,9 +284,9 @@ class FakeSnowflakeCursor:
287
284
  (affected_count,) = self._duck_conn.fetchall()[0]
288
285
  result_sql = SQL_DELETED_ROWS.substitute(count=affected_count)
289
286
 
290
- elif cmd == "DESCRIBE TABLE":
291
- # DESCRIBE TABLE has already been run above to detect and error if the table exists
292
- # We now rerun DESCRIBE TABLE but transformed with columns to match Snowflake
287
+ elif cmd in ("DESCRIBE TABLE", "DESCRIBE VIEW"):
288
+ # DESCRIBE TABLE/VIEW has already been run above to detect and error if the table exists
289
+ # We now rerun DESCRIBE TABLE/VIEW but transformed with columns to match Snowflake
293
290
  result_sql = transformed.transform(
294
291
  lambda e: transforms.describe_table(e, self._conn.database, self._conn.schema)
295
292
  ).sql(dialect="duckdb")
@@ -337,6 +334,7 @@ class FakeSnowflakeCursor:
337
334
  self._duck_conn.execute(info_schema.insert_text_lengths_sql(catalog, schema, table.name, text_lengths))
338
335
 
339
336
  if result_sql:
337
+ self._log_sql(result_sql, params)
340
338
  self._duck_conn.execute(result_sql)
341
339
 
342
340
  self._arrow_table = self._duck_conn.fetch_arrow_table()
@@ -347,6 +345,10 @@ class FakeSnowflakeCursor:
347
345
 
348
346
  return self
349
347
 
348
+ def _log_sql(self, sql: str, params: Sequence[Any] | dict[Any, Any] | None = None) -> None:
349
+ if (fs_debug := os.environ.get("FAKESNOW_DEBUG")) and fs_debug != "snowflake":
350
+ print(f"{sql};{params=}" if params else f"{sql};", file=sys.stderr)
351
+
350
352
  def executemany(
351
353
  self,
352
354
  command: str,
@@ -389,12 +391,13 @@ class FakeSnowflakeCursor:
389
391
  if self._arrow_table is None:
390
392
  # mimic snowflake python connector error type
391
393
  raise TypeError("No open result set")
394
+ tslice = self._arrow_table.slice(offset=self._arrow_table_fetch_index or 0, length=size).to_pylist()
395
+
392
396
  if self._arrow_table_fetch_index is None:
393
- self._arrow_table_fetch_index = 0
397
+ self._arrow_table_fetch_index = size
394
398
  else:
395
399
  self._arrow_table_fetch_index += size
396
400
 
397
- tslice = self._arrow_table.slice(offset=self._arrow_table_fetch_index, length=size).to_pylist()
398
401
  return tslice if self._use_dict_result else [tuple(d.values()) for d in tslice]
399
402
 
400
403
  def get_result_batches(self) -> list[ResultBatch] | None:
@@ -78,6 +78,7 @@ LEFT JOIN duckdb_columns ddb_columns
78
78
  """
79
79
  )
80
80
 
81
+
81
82
  # replicates https://docs.snowflake.com/sql-reference/info-schema/databases
82
83
  SQL_CREATE_INFORMATION_SCHEMA_DATABASES_VIEW = Template(
83
84
  """
@@ -103,6 +103,7 @@ routes = [
103
103
  query_request,
104
104
  methods=["POST"],
105
105
  ),
106
+ Route("/queries/v1/abort-request", lambda _: JSONResponse({"success": True}), methods=["POST"]),
106
107
  ]
107
108
 
108
109
  app = Starlette(debug=True, routes=routes)
@@ -159,22 +159,41 @@ SELECT
159
159
  column_default AS "default",
160
160
  'N' AS "primary key",
161
161
  'N' AS "unique key",
162
- NULL AS "check",
163
- NULL AS "expression",
164
- NULL AS "comment",
165
- NULL AS "policy name",
166
- NULL AS "privacy domain",
162
+ NULL::VARCHAR AS "check",
163
+ NULL::VARCHAR AS "expression",
164
+ NULL::VARCHAR AS "comment",
165
+ NULL::VARCHAR AS "policy name",
166
+ NULL::JSON AS "privacy domain",
167
167
  FROM information_schema._fs_columns_snowflake
168
168
  WHERE table_catalog = '${catalog}' AND table_schema = '${schema}' AND table_name = '${table}'
169
169
  ORDER BY ordinal_position
170
170
  """
171
171
  )
172
172
 
173
+ SQL_DESCRIBE_INFO_SCHEMA = Template(
174
+ """
175
+ SELECT
176
+ column_name AS "name",
177
+ column_type as "type",
178
+ 'COLUMN' AS "kind",
179
+ CASE WHEN "null" = 'YES' THEN 'Y' ELSE 'N' END AS "null?",
180
+ NULL::VARCHAR AS "default",
181
+ 'N' AS "primary key",
182
+ 'N' AS "unique key",
183
+ NULL::VARCHAR AS "check",
184
+ NULL::VARCHAR AS "expression",
185
+ NULL::VARCHAR AS "comment",
186
+ NULL::VARCHAR AS "policy name",
187
+ NULL::JSON AS "privacy domain",
188
+ FROM (DESCRIBE information_schema.${view})
189
+ """
190
+ )
191
+
173
192
 
174
193
  def describe_table(
175
194
  expression: exp.Expression, current_database: str | None = None, current_schema: str | None = None
176
195
  ) -> exp.Expression:
177
- """Redirect to the information_schema._fs_describe_table to match snowflake.
196
+ """Redirect to the information_schema._fs_columns_snowflake to match snowflake.
178
197
 
179
198
  See https://docs.snowflake.com/en/sql-reference/sql/desc-table
180
199
  """
@@ -183,12 +202,16 @@ def describe_table(
183
202
  isinstance(expression, exp.Describe)
184
203
  and (kind := expression.args.get("kind"))
185
204
  and isinstance(kind, str)
186
- and kind.upper() == "TABLE"
205
+ and kind.upper() in ("TABLE", "VIEW")
187
206
  and (table := expression.find(exp.Table))
188
207
  ):
189
208
  catalog = table.catalog or current_database
190
209
  schema = table.db or current_schema
191
210
 
211
+ if schema and schema.upper() == "INFORMATION_SCHEMA":
212
+ # information schema views don't exist in _fs_columns_snowflake
213
+ return sqlglot.parse_one(SQL_DESCRIBE_INFO_SCHEMA.substitute(view=table.name), read="duckdb")
214
+
192
215
  return sqlglot.parse_one(
193
216
  SQL_DESCRIBE_TABLE.substitute(catalog=catalog, schema=schema, table=table.name),
194
217
  read="duckdb",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fakesnow
3
- Version: 0.9.21
3
+ Version: 0.9.22
4
4
  Summary: Fake Snowflake Connector for Python. Run, mock and test Snowflake DB locally.
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -213,7 +213,7 @@ License-File: LICENSE
213
213
  Requires-Dist: duckdb~=1.0.0
214
214
  Requires-Dist: pyarrow
215
215
  Requires-Dist: snowflake-connector-python
216
- Requires-Dist: sqlglot~=25.5.1
216
+ Requires-Dist: sqlglot~=25.9.0
217
217
  Provides-Extra: dev
218
218
  Requires-Dist: build~=1.0; extra == "dev"
219
219
  Requires-Dist: pandas-stubs; extra == "dev"
@@ -1,7 +1,7 @@
1
1
  duckdb~=1.0.0
2
2
  pyarrow
3
3
  snowflake-connector-python
4
- sqlglot~=25.5.1
4
+ sqlglot~=25.9.0
5
5
 
6
6
  [dev]
7
7
  build~=1.0
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "fakesnow"
3
3
  description = "Fake Snowflake Connector for Python. Run, mock and test Snowflake DB locally."
4
- version = "0.9.21"
4
+ version = "0.9.22"
5
5
  readme = "README.md"
6
6
  license = { file = "LICENSE" }
7
7
  classifiers = ["License :: OSI Approved :: MIT License"]
@@ -11,7 +11,7 @@ dependencies = [
11
11
  "duckdb~=1.0.0",
12
12
  "pyarrow",
13
13
  "snowflake-connector-python",
14
- "sqlglot~=25.5.1",
14
+ "sqlglot~=25.9.0",
15
15
  ]
16
16
 
17
17
  [project.urls]
@@ -62,6 +62,9 @@ reportUnnecessaryTypeIgnoreComment = true
62
62
 
63
63
  [tool.pytest.ini_options]
64
64
  asyncio_mode = "auto"
65
+ # error on unhandled exceptions in background threads
66
+ # useful for catching errors in server or snowflake connector threads
67
+ filterwarnings = ["error::pytest.PytestUnhandledThreadExceptionWarning"]
65
68
 
66
69
  [tool.ruff]
67
70
  line-length = 120
@@ -436,6 +436,52 @@ def test_describe_table(dcur: snowflake.connector.cursor.DictCursor):
436
436
  assert "002003 (42S02): Catalog Error: Table with name THIS_DOES_NOT_EXIST does not exist!" in str(excinfo.value)
437
437
 
438
438
 
439
+ def test_describe_view(dcur: snowflake.connector.cursor.DictCursor):
440
+ dcur.execute(
441
+ """
442
+ create or replace table example (
443
+ XVARCHAR VARCHAR
444
+ -- ,XVARCHAR20 VARCHAR(20) -- TODO: preserve varchar size
445
+ )
446
+ """
447
+ )
448
+
449
+ common = {
450
+ "kind": "COLUMN",
451
+ "null?": "Y",
452
+ "default": None,
453
+ "primary key": "N",
454
+ "unique key": "N",
455
+ "check": None,
456
+ "expression": None,
457
+ "comment": None,
458
+ "policy name": None,
459
+ "privacy domain": None,
460
+ }
461
+ expected = [
462
+ {"name": "XVARCHAR", "type": "VARCHAR(16777216)", **common},
463
+ # TODO: preserve varchar size
464
+ # {"name": "XVARCHAR20", "type": "VARCHAR(20)", **common},
465
+ ]
466
+
467
+ dcur.execute("create view v1 as select * from example")
468
+ assert dcur.execute("describe view v1").fetchall() == expected
469
+ assert [r.name for r in dcur.description] == [
470
+ "name",
471
+ "type",
472
+ "kind",
473
+ "null?",
474
+ "default",
475
+ "primary key",
476
+ "unique key",
477
+ "check",
478
+ "expression",
479
+ "comment",
480
+ "policy name",
481
+ "privacy domain",
482
+ ]
483
+
484
+
439
485
  ## descriptions are needed for ipython-sql/jupysql which describes every statement
440
486
  def test_description_create_drop_database(dcur: snowflake.connector.cursor.DictCursor):
441
487
  dcur.execute("create database example")
@@ -605,9 +651,10 @@ def test_fetchmany(conn: snowflake.connector.SnowflakeConnection):
605
651
  cur.execute("insert into customers values (3, 'Jeremy', 'K')")
606
652
  cur.execute("select id, first_name, last_name from customers")
607
653
 
654
+ # mimic jupysql fetchmany behaviour
608
655
  assert cur.fetchmany(2) == [(1, "Jenny", "P"), (2, "Jasper", "M")]
609
- assert cur.fetchmany(2) == [(3, "Jeremy", "K")]
610
- assert cur.fetchmany(2) == []
656
+ assert cur.fetchmany(5) == [(3, "Jeremy", "K")]
657
+ assert cur.fetchmany(5) == []
611
658
 
612
659
  with conn.cursor(snowflake.connector.cursor.DictCursor) as cur:
613
660
  cur.execute("select id, first_name, last_name from customers")
@@ -615,10 +662,10 @@ def test_fetchmany(conn: snowflake.connector.SnowflakeConnection):
615
662
  {"ID": 1, "FIRST_NAME": "Jenny", "LAST_NAME": "P"},
616
663
  {"ID": 2, "FIRST_NAME": "Jasper", "LAST_NAME": "M"},
617
664
  ]
618
- assert cur.fetchmany(2) == [
665
+ assert cur.fetchmany(5) == [
619
666
  {"ID": 3, "FIRST_NAME": "Jeremy", "LAST_NAME": "K"},
620
667
  ]
621
- assert cur.fetchmany(2) == []
668
+ assert cur.fetchmany(5) == []
622
669
 
623
670
 
624
671
  def test_fetch_pandas_all(cur: snowflake.connector.cursor.SnowflakeCursor):
@@ -38,11 +38,45 @@ def test_info_schema_columns_describe(cur: snowflake.connector.cursor.SnowflakeC
38
38
 
39
39
  assert cur.description == expected_metadata
40
40
 
41
+
42
+ def test_describe_view_columns(dcur: snowflake.connector.cursor.DictCursor):
43
+ cols = [
44
+ "name",
45
+ "type",
46
+ "kind",
47
+ "null?",
48
+ "default",
49
+ "primary key",
50
+ "unique key",
51
+ "check",
52
+ "expression",
53
+ "comment",
54
+ "policy name",
55
+ "privacy domain",
56
+ ]
57
+ dcur.execute("describe view information_schema.columns")
58
+ result: list[dict] = dcur.fetchall() # type: ignore
59
+ assert list(result[0].keys()) == cols
60
+ names = [r["name"] for r in result]
41
61
  # should contain snowflake-specific columns (from _FS_COLUMNS_SNOWFLAKE)
42
- cur.execute("describe view information_schema.columns")
43
- result = cur.fetchall()
44
- names = [name for (name, *_) in result]
45
62
  assert "comment" in names
63
+ # fmt: off
64
+ assert dcur.description[:-1] == [
65
+ ResultMetadata(name='name', type_code=2, display_size=None, internal_size=16777216, precision=None, scale=None, is_nullable=True),
66
+ ResultMetadata(name='type', type_code=2, display_size=None, internal_size=16777216, precision=None, scale=None, is_nullable=True),
67
+ ResultMetadata(name='kind', type_code=2, display_size=None, internal_size=16777216, precision=None, scale=None, is_nullable=True),
68
+ ResultMetadata(name='null?', type_code=2, display_size=None, internal_size=16777216, precision=None, scale=None, is_nullable=True),
69
+ ResultMetadata(name='default', type_code=2, display_size=None, internal_size=16777216, precision=None, scale=None, is_nullable=True),
70
+ ResultMetadata(name='primary key', type_code=2, display_size=None, internal_size=16777216, precision=None, scale=None, is_nullable=True),
71
+ ResultMetadata(name='unique key', type_code=2, display_size=None, internal_size=16777216, precision=None, scale=None, is_nullable=True),
72
+ ResultMetadata(name='check', type_code=2, display_size=None, internal_size=16777216, precision=None, scale=None, is_nullable=True),
73
+ ResultMetadata(name='expression', type_code=2, display_size=None, internal_size=16777216, precision=None, scale=None, is_nullable=True),
74
+ ResultMetadata(name='comment', type_code=2, display_size=None, internal_size=16777216, precision=None, scale=None, is_nullable=True),
75
+ ResultMetadata(name='policy name', type_code=2, display_size=None, internal_size=16777216, precision=None, scale=None, is_nullable=True),
76
+ # TODO: ignore the following, see https://github.com/tekumara/fakesnow/issues/26
77
+ # ResultMetadata(name='privacy domain', type_code=9, display_size=None, internal_size=16777216, precision=None, scale=None, is_nullable=True)
78
+ ]
79
+ # fmt: on
46
80
 
47
81
 
48
82
  def test_info_schema_columns_numeric(cur: snowflake.connector.cursor.SnowflakeCursor):
@@ -18,7 +18,7 @@ def unused_port(unused_tcp_port_factory: Callable[[], int]) -> int:
18
18
 
19
19
 
20
20
  @pytest.fixture(scope="session")
21
- def server(unused_tcp_port_factory: Callable[[], int]) -> Iterator[int]:
21
+ def server(unused_tcp_port_factory: Callable[[], int]) -> Iterator[dict]:
22
22
  port = unused_tcp_port_factory()
23
23
  server = uvicorn.Server(uvicorn.Config(fakesnow.server.app, port=port, log_level="info"))
24
24
  thread = threading.Thread(target=server.run, name="Server", daemon=True)
@@ -26,28 +26,42 @@ def server(unused_tcp_port_factory: Callable[[], int]) -> Iterator[int]:
26
26
 
27
27
  while not server.started:
28
28
  sleep(0.1)
29
- yield port
29
+ yield dict(
30
+ user="fake",
31
+ password="snow",
32
+ account="fakesnow",
33
+ host="localhost",
34
+ port=port,
35
+ protocol="http",
36
+ # disable telemetry
37
+ session_parameters={"CLIENT_OUT_OF_BAND_TELEMETRY_ENABLED": False},
38
+ )
30
39
 
31
40
  server.should_exit = True
32
41
  # wait for server thread to end
33
42
  thread.join()
34
43
 
35
44
 
36
- def test_server_connect(server: int) -> None:
45
+ def test_server_connect(server: dict) -> None:
37
46
  with (
38
47
  snowflake.connector.connect(
39
- user="fake",
40
- password="snow",
41
- account="fakesnow",
42
- host="localhost",
43
- port=server,
44
- protocol="http",
45
- # disable telemetry
46
- session_parameters={"CLIENT_OUT_OF_BAND_TELEMETRY_ENABLED": False},
48
+ **server,
47
49
  # disable infinite retries on error
48
- network_timeout=0,
50
+ network_timeout=1,
49
51
  ) as conn1,
50
52
  conn1.cursor() as cur,
51
53
  ):
52
54
  cur.execute("select 'hello', to_decimal('12.3456', 10,2)")
53
55
  assert cur.fetchall() == [("hello", Decimal("12.35"))]
56
+
57
+
58
+ def test_server_abort_request(server: dict) -> None:
59
+ with (
60
+ snowflake.connector.connect(
61
+ **server,
62
+ # triggers an abort request
63
+ network_timeout=0,
64
+ ) as conn1,
65
+ conn1.cursor() as cur,
66
+ ):
67
+ cur.execute("select 'will abort'")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes