sqlspec 0.36.0__cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl
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.
- ac8f31065839703b4e70__mypyc.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/__init__.py +140 -0
- sqlspec/__main__.py +12 -0
- sqlspec/__metadata__.py +14 -0
- sqlspec/_serialization.py +315 -0
- sqlspec/_typing.py +700 -0
- sqlspec/adapters/__init__.py +0 -0
- sqlspec/adapters/adbc/__init__.py +5 -0
- sqlspec/adapters/adbc/_typing.py +82 -0
- sqlspec/adapters/adbc/adk/__init__.py +5 -0
- sqlspec/adapters/adbc/adk/store.py +1273 -0
- sqlspec/adapters/adbc/config.py +295 -0
- sqlspec/adapters/adbc/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/adbc/core.py +735 -0
- sqlspec/adapters/adbc/data_dictionary.py +334 -0
- sqlspec/adapters/adbc/driver.py +529 -0
- sqlspec/adapters/adbc/events/__init__.py +5 -0
- sqlspec/adapters/adbc/events/store.py +285 -0
- sqlspec/adapters/adbc/litestar/__init__.py +5 -0
- sqlspec/adapters/adbc/litestar/store.py +502 -0
- sqlspec/adapters/adbc/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/adbc/type_converter.py +140 -0
- sqlspec/adapters/aiosqlite/__init__.py +25 -0
- sqlspec/adapters/aiosqlite/_typing.py +82 -0
- sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/adk/store.py +818 -0
- sqlspec/adapters/aiosqlite/config.py +334 -0
- sqlspec/adapters/aiosqlite/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/aiosqlite/core.py +315 -0
- sqlspec/adapters/aiosqlite/data_dictionary.py +208 -0
- sqlspec/adapters/aiosqlite/driver.py +313 -0
- sqlspec/adapters/aiosqlite/events/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/events/store.py +20 -0
- sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/litestar/store.py +279 -0
- sqlspec/adapters/aiosqlite/pool.py +533 -0
- sqlspec/adapters/asyncmy/__init__.py +21 -0
- sqlspec/adapters/asyncmy/_typing.py +87 -0
- sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
- sqlspec/adapters/asyncmy/adk/store.py +703 -0
- sqlspec/adapters/asyncmy/config.py +302 -0
- sqlspec/adapters/asyncmy/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/asyncmy/core.py +360 -0
- sqlspec/adapters/asyncmy/data_dictionary.py +124 -0
- sqlspec/adapters/asyncmy/driver.py +383 -0
- sqlspec/adapters/asyncmy/events/__init__.py +5 -0
- sqlspec/adapters/asyncmy/events/store.py +104 -0
- sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncmy/litestar/store.py +296 -0
- sqlspec/adapters/asyncpg/__init__.py +19 -0
- sqlspec/adapters/asyncpg/_typing.py +88 -0
- sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
- sqlspec/adapters/asyncpg/adk/store.py +748 -0
- sqlspec/adapters/asyncpg/config.py +569 -0
- sqlspec/adapters/asyncpg/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/asyncpg/core.py +367 -0
- sqlspec/adapters/asyncpg/data_dictionary.py +162 -0
- sqlspec/adapters/asyncpg/driver.py +487 -0
- sqlspec/adapters/asyncpg/events/__init__.py +6 -0
- sqlspec/adapters/asyncpg/events/backend.py +286 -0
- sqlspec/adapters/asyncpg/events/store.py +40 -0
- sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncpg/litestar/store.py +251 -0
- sqlspec/adapters/bigquery/__init__.py +14 -0
- sqlspec/adapters/bigquery/_typing.py +86 -0
- sqlspec/adapters/bigquery/adk/__init__.py +5 -0
- sqlspec/adapters/bigquery/adk/store.py +827 -0
- sqlspec/adapters/bigquery/config.py +353 -0
- sqlspec/adapters/bigquery/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/bigquery/core.py +715 -0
- sqlspec/adapters/bigquery/data_dictionary.py +128 -0
- sqlspec/adapters/bigquery/driver.py +548 -0
- sqlspec/adapters/bigquery/events/__init__.py +5 -0
- sqlspec/adapters/bigquery/events/store.py +139 -0
- sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
- sqlspec/adapters/bigquery/litestar/store.py +325 -0
- sqlspec/adapters/bigquery/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/bigquery/type_converter.py +107 -0
- sqlspec/adapters/cockroach_asyncpg/__init__.py +24 -0
- sqlspec/adapters/cockroach_asyncpg/_typing.py +72 -0
- sqlspec/adapters/cockroach_asyncpg/adk/__init__.py +3 -0
- sqlspec/adapters/cockroach_asyncpg/adk/store.py +410 -0
- sqlspec/adapters/cockroach_asyncpg/config.py +238 -0
- sqlspec/adapters/cockroach_asyncpg/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/cockroach_asyncpg/core.py +55 -0
- sqlspec/adapters/cockroach_asyncpg/data_dictionary.py +107 -0
- sqlspec/adapters/cockroach_asyncpg/driver.py +144 -0
- sqlspec/adapters/cockroach_asyncpg/events/__init__.py +3 -0
- sqlspec/adapters/cockroach_asyncpg/events/store.py +20 -0
- sqlspec/adapters/cockroach_asyncpg/litestar/__init__.py +3 -0
- sqlspec/adapters/cockroach_asyncpg/litestar/store.py +142 -0
- sqlspec/adapters/cockroach_psycopg/__init__.py +38 -0
- sqlspec/adapters/cockroach_psycopg/_typing.py +129 -0
- sqlspec/adapters/cockroach_psycopg/adk/__init__.py +13 -0
- sqlspec/adapters/cockroach_psycopg/adk/store.py +868 -0
- sqlspec/adapters/cockroach_psycopg/config.py +484 -0
- sqlspec/adapters/cockroach_psycopg/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/cockroach_psycopg/core.py +63 -0
- sqlspec/adapters/cockroach_psycopg/data_dictionary.py +215 -0
- sqlspec/adapters/cockroach_psycopg/driver.py +284 -0
- sqlspec/adapters/cockroach_psycopg/events/__init__.py +6 -0
- sqlspec/adapters/cockroach_psycopg/events/store.py +34 -0
- sqlspec/adapters/cockroach_psycopg/litestar/__init__.py +3 -0
- sqlspec/adapters/cockroach_psycopg/litestar/store.py +325 -0
- sqlspec/adapters/duckdb/__init__.py +25 -0
- sqlspec/adapters/duckdb/_typing.py +81 -0
- sqlspec/adapters/duckdb/adk/__init__.py +14 -0
- sqlspec/adapters/duckdb/adk/store.py +850 -0
- sqlspec/adapters/duckdb/config.py +463 -0
- sqlspec/adapters/duckdb/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/duckdb/core.py +257 -0
- sqlspec/adapters/duckdb/data_dictionary.py +140 -0
- sqlspec/adapters/duckdb/driver.py +430 -0
- sqlspec/adapters/duckdb/events/__init__.py +5 -0
- sqlspec/adapters/duckdb/events/store.py +57 -0
- sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
- sqlspec/adapters/duckdb/litestar/store.py +330 -0
- sqlspec/adapters/duckdb/pool.py +293 -0
- sqlspec/adapters/duckdb/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/duckdb/type_converter.py +118 -0
- sqlspec/adapters/mock/__init__.py +72 -0
- sqlspec/adapters/mock/_typing.py +147 -0
- sqlspec/adapters/mock/config.py +483 -0
- sqlspec/adapters/mock/core.py +319 -0
- sqlspec/adapters/mock/data_dictionary.py +366 -0
- sqlspec/adapters/mock/driver.py +721 -0
- sqlspec/adapters/mysqlconnector/__init__.py +36 -0
- sqlspec/adapters/mysqlconnector/_typing.py +141 -0
- sqlspec/adapters/mysqlconnector/adk/__init__.py +15 -0
- sqlspec/adapters/mysqlconnector/adk/store.py +1060 -0
- sqlspec/adapters/mysqlconnector/config.py +394 -0
- sqlspec/adapters/mysqlconnector/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/mysqlconnector/core.py +303 -0
- sqlspec/adapters/mysqlconnector/data_dictionary.py +235 -0
- sqlspec/adapters/mysqlconnector/driver.py +483 -0
- sqlspec/adapters/mysqlconnector/events/__init__.py +8 -0
- sqlspec/adapters/mysqlconnector/events/store.py +98 -0
- sqlspec/adapters/mysqlconnector/litestar/__init__.py +5 -0
- sqlspec/adapters/mysqlconnector/litestar/store.py +426 -0
- sqlspec/adapters/oracledb/__init__.py +60 -0
- sqlspec/adapters/oracledb/_numpy_handlers.py +141 -0
- sqlspec/adapters/oracledb/_typing.py +182 -0
- sqlspec/adapters/oracledb/_uuid_handlers.py +166 -0
- sqlspec/adapters/oracledb/adk/__init__.py +10 -0
- sqlspec/adapters/oracledb/adk/store.py +2369 -0
- sqlspec/adapters/oracledb/config.py +550 -0
- sqlspec/adapters/oracledb/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/oracledb/core.py +543 -0
- sqlspec/adapters/oracledb/data_dictionary.py +536 -0
- sqlspec/adapters/oracledb/driver.py +1229 -0
- sqlspec/adapters/oracledb/events/__init__.py +16 -0
- sqlspec/adapters/oracledb/events/backend.py +347 -0
- sqlspec/adapters/oracledb/events/store.py +420 -0
- sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
- sqlspec/adapters/oracledb/litestar/store.py +781 -0
- sqlspec/adapters/oracledb/migrations.py +535 -0
- sqlspec/adapters/oracledb/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/oracledb/type_converter.py +211 -0
- sqlspec/adapters/psqlpy/__init__.py +17 -0
- sqlspec/adapters/psqlpy/_typing.py +79 -0
- sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
- sqlspec/adapters/psqlpy/adk/store.py +766 -0
- sqlspec/adapters/psqlpy/config.py +304 -0
- sqlspec/adapters/psqlpy/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/psqlpy/core.py +480 -0
- sqlspec/adapters/psqlpy/data_dictionary.py +126 -0
- sqlspec/adapters/psqlpy/driver.py +438 -0
- sqlspec/adapters/psqlpy/events/__init__.py +6 -0
- sqlspec/adapters/psqlpy/events/backend.py +310 -0
- sqlspec/adapters/psqlpy/events/store.py +20 -0
- sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
- sqlspec/adapters/psqlpy/litestar/store.py +270 -0
- sqlspec/adapters/psqlpy/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/psqlpy/type_converter.py +113 -0
- sqlspec/adapters/psycopg/__init__.py +32 -0
- sqlspec/adapters/psycopg/_typing.py +164 -0
- sqlspec/adapters/psycopg/adk/__init__.py +10 -0
- sqlspec/adapters/psycopg/adk/store.py +1387 -0
- sqlspec/adapters/psycopg/config.py +576 -0
- sqlspec/adapters/psycopg/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/psycopg/core.py +450 -0
- sqlspec/adapters/psycopg/data_dictionary.py +289 -0
- sqlspec/adapters/psycopg/driver.py +975 -0
- sqlspec/adapters/psycopg/events/__init__.py +20 -0
- sqlspec/adapters/psycopg/events/backend.py +458 -0
- sqlspec/adapters/psycopg/events/store.py +42 -0
- sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
- sqlspec/adapters/psycopg/litestar/store.py +552 -0
- sqlspec/adapters/psycopg/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/psycopg/type_converter.py +93 -0
- sqlspec/adapters/pymysql/__init__.py +21 -0
- sqlspec/adapters/pymysql/_typing.py +71 -0
- sqlspec/adapters/pymysql/adk/__init__.py +5 -0
- sqlspec/adapters/pymysql/adk/store.py +540 -0
- sqlspec/adapters/pymysql/config.py +195 -0
- sqlspec/adapters/pymysql/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/pymysql/core.py +299 -0
- sqlspec/adapters/pymysql/data_dictionary.py +122 -0
- sqlspec/adapters/pymysql/driver.py +259 -0
- sqlspec/adapters/pymysql/events/__init__.py +5 -0
- sqlspec/adapters/pymysql/events/store.py +50 -0
- sqlspec/adapters/pymysql/litestar/__init__.py +5 -0
- sqlspec/adapters/pymysql/litestar/store.py +232 -0
- sqlspec/adapters/pymysql/pool.py +137 -0
- sqlspec/adapters/spanner/__init__.py +40 -0
- sqlspec/adapters/spanner/_typing.py +86 -0
- sqlspec/adapters/spanner/adk/__init__.py +5 -0
- sqlspec/adapters/spanner/adk/store.py +732 -0
- sqlspec/adapters/spanner/config.py +352 -0
- sqlspec/adapters/spanner/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/spanner/core.py +188 -0
- sqlspec/adapters/spanner/data_dictionary.py +120 -0
- sqlspec/adapters/spanner/dialect/__init__.py +6 -0
- sqlspec/adapters/spanner/dialect/_spangres.py +57 -0
- sqlspec/adapters/spanner/dialect/_spanner.py +130 -0
- sqlspec/adapters/spanner/driver.py +373 -0
- sqlspec/adapters/spanner/events/__init__.py +5 -0
- sqlspec/adapters/spanner/events/store.py +187 -0
- sqlspec/adapters/spanner/litestar/__init__.py +5 -0
- sqlspec/adapters/spanner/litestar/store.py +291 -0
- sqlspec/adapters/spanner/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/spanner/type_converter.py +331 -0
- sqlspec/adapters/sqlite/__init__.py +19 -0
- sqlspec/adapters/sqlite/_typing.py +80 -0
- sqlspec/adapters/sqlite/adk/__init__.py +5 -0
- sqlspec/adapters/sqlite/adk/store.py +958 -0
- sqlspec/adapters/sqlite/config.py +280 -0
- sqlspec/adapters/sqlite/core.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/sqlite/core.py +312 -0
- sqlspec/adapters/sqlite/data_dictionary.py +202 -0
- sqlspec/adapters/sqlite/driver.py +359 -0
- sqlspec/adapters/sqlite/events/__init__.py +5 -0
- sqlspec/adapters/sqlite/events/store.py +20 -0
- sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/sqlite/litestar/store.py +316 -0
- sqlspec/adapters/sqlite/pool.py +198 -0
- sqlspec/adapters/sqlite/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/adapters/sqlite/type_converter.py +114 -0
- sqlspec/base.py +747 -0
- sqlspec/builder/__init__.py +179 -0
- sqlspec/builder/_base.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_base.py +1022 -0
- sqlspec/builder/_column.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_column.py +521 -0
- sqlspec/builder/_ddl.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_ddl.py +1642 -0
- sqlspec/builder/_delete.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_delete.py +95 -0
- sqlspec/builder/_dml.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_dml.py +365 -0
- sqlspec/builder/_explain.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_explain.py +579 -0
- sqlspec/builder/_expression_wrappers.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_expression_wrappers.py +46 -0
- sqlspec/builder/_factory.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_factory.py +1697 -0
- sqlspec/builder/_insert.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_insert.py +328 -0
- sqlspec/builder/_join.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_join.py +499 -0
- sqlspec/builder/_merge.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_merge.py +821 -0
- sqlspec/builder/_parsing_utils.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_parsing_utils.py +297 -0
- sqlspec/builder/_select.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_select.py +1660 -0
- sqlspec/builder/_temporal.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_temporal.py +139 -0
- sqlspec/builder/_update.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/builder/_update.py +173 -0
- sqlspec/builder/_vector_expressions.py +267 -0
- sqlspec/cli.py +911 -0
- sqlspec/config.py +1755 -0
- sqlspec/core/__init__.py +374 -0
- sqlspec/core/_correlation.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/_correlation.py +176 -0
- sqlspec/core/cache.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/cache.py +1069 -0
- sqlspec/core/compiler.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/compiler.py +954 -0
- sqlspec/core/explain.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/explain.py +275 -0
- sqlspec/core/filters.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/filters.py +952 -0
- sqlspec/core/hashing.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/hashing.py +262 -0
- sqlspec/core/metrics.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/metrics.py +83 -0
- sqlspec/core/parameters/__init__.py +71 -0
- sqlspec/core/parameters/_alignment.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/parameters/_alignment.py +270 -0
- sqlspec/core/parameters/_converter.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/parameters/_converter.py +543 -0
- sqlspec/core/parameters/_processor.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/parameters/_processor.py +505 -0
- sqlspec/core/parameters/_registry.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/parameters/_registry.py +206 -0
- sqlspec/core/parameters/_transformers.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/parameters/_transformers.py +292 -0
- sqlspec/core/parameters/_types.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/parameters/_types.py +499 -0
- sqlspec/core/parameters/_validator.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/parameters/_validator.py +180 -0
- sqlspec/core/pipeline.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/pipeline.py +319 -0
- sqlspec/core/query_modifiers.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/query_modifiers.py +437 -0
- sqlspec/core/result/__init__.py +23 -0
- sqlspec/core/result/_base.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/result/_base.py +1121 -0
- sqlspec/core/result/_io.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/result/_io.py +28 -0
- sqlspec/core/splitter.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/splitter.py +966 -0
- sqlspec/core/stack.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/stack.py +163 -0
- sqlspec/core/statement.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/statement.py +1503 -0
- sqlspec/core/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/core/type_converter.py +339 -0
- sqlspec/data_dictionary/__init__.py +22 -0
- sqlspec/data_dictionary/_loader.py +123 -0
- sqlspec/data_dictionary/_registry.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/data_dictionary/_registry.py +74 -0
- sqlspec/data_dictionary/_types.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/data_dictionary/_types.py +121 -0
- sqlspec/data_dictionary/dialects/__init__.py +21 -0
- sqlspec/data_dictionary/dialects/bigquery.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/data_dictionary/dialects/bigquery.py +49 -0
- sqlspec/data_dictionary/dialects/cockroachdb.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/data_dictionary/dialects/cockroachdb.py +43 -0
- sqlspec/data_dictionary/dialects/duckdb.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/data_dictionary/dialects/duckdb.py +47 -0
- sqlspec/data_dictionary/dialects/mysql.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/data_dictionary/dialects/mysql.py +42 -0
- sqlspec/data_dictionary/dialects/oracle.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/data_dictionary/dialects/oracle.py +34 -0
- sqlspec/data_dictionary/dialects/postgres.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/data_dictionary/dialects/postgres.py +46 -0
- sqlspec/data_dictionary/dialects/spanner.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/data_dictionary/dialects/spanner.py +37 -0
- sqlspec/data_dictionary/dialects/sqlite.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/data_dictionary/dialects/sqlite.py +42 -0
- sqlspec/data_dictionary/sql/.gitkeep +0 -0
- sqlspec/data_dictionary/sql/bigquery/columns.sql +23 -0
- sqlspec/data_dictionary/sql/bigquery/foreign_keys.sql +34 -0
- sqlspec/data_dictionary/sql/bigquery/indexes.sql +19 -0
- sqlspec/data_dictionary/sql/bigquery/tables.sql +33 -0
- sqlspec/data_dictionary/sql/bigquery/version.sql +3 -0
- sqlspec/data_dictionary/sql/cockroachdb/columns.sql +34 -0
- sqlspec/data_dictionary/sql/cockroachdb/foreign_keys.sql +40 -0
- sqlspec/data_dictionary/sql/cockroachdb/indexes.sql +32 -0
- sqlspec/data_dictionary/sql/cockroachdb/tables.sql +44 -0
- sqlspec/data_dictionary/sql/cockroachdb/version.sql +3 -0
- sqlspec/data_dictionary/sql/duckdb/columns.sql +23 -0
- sqlspec/data_dictionary/sql/duckdb/foreign_keys.sql +36 -0
- sqlspec/data_dictionary/sql/duckdb/indexes.sql +19 -0
- sqlspec/data_dictionary/sql/duckdb/tables.sql +38 -0
- sqlspec/data_dictionary/sql/duckdb/version.sql +3 -0
- sqlspec/data_dictionary/sql/mysql/columns.sql +23 -0
- sqlspec/data_dictionary/sql/mysql/foreign_keys.sql +28 -0
- sqlspec/data_dictionary/sql/mysql/indexes.sql +26 -0
- sqlspec/data_dictionary/sql/mysql/tables.sql +33 -0
- sqlspec/data_dictionary/sql/mysql/version.sql +3 -0
- sqlspec/data_dictionary/sql/oracle/columns.sql +23 -0
- sqlspec/data_dictionary/sql/oracle/foreign_keys.sql +48 -0
- sqlspec/data_dictionary/sql/oracle/indexes.sql +44 -0
- sqlspec/data_dictionary/sql/oracle/tables.sql +25 -0
- sqlspec/data_dictionary/sql/oracle/version.sql +20 -0
- sqlspec/data_dictionary/sql/postgres/columns.sql +34 -0
- sqlspec/data_dictionary/sql/postgres/foreign_keys.sql +40 -0
- sqlspec/data_dictionary/sql/postgres/indexes.sql +56 -0
- sqlspec/data_dictionary/sql/postgres/tables.sql +44 -0
- sqlspec/data_dictionary/sql/postgres/version.sql +3 -0
- sqlspec/data_dictionary/sql/spanner/columns.sql +23 -0
- sqlspec/data_dictionary/sql/spanner/foreign_keys.sql +70 -0
- sqlspec/data_dictionary/sql/spanner/indexes.sql +30 -0
- sqlspec/data_dictionary/sql/spanner/tables.sql +9 -0
- sqlspec/data_dictionary/sql/spanner/version.sql +3 -0
- sqlspec/data_dictionary/sql/sqlite/columns.sql +23 -0
- sqlspec/data_dictionary/sql/sqlite/foreign_keys.sql +22 -0
- sqlspec/data_dictionary/sql/sqlite/indexes.sql +7 -0
- sqlspec/data_dictionary/sql/sqlite/tables.sql +28 -0
- sqlspec/data_dictionary/sql/sqlite/version.sql +3 -0
- sqlspec/driver/__init__.py +32 -0
- sqlspec/driver/_async.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/driver/_async.py +1737 -0
- sqlspec/driver/_common.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/driver/_common.py +1478 -0
- sqlspec/driver/_sql_helpers.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/driver/_sql_helpers.py +148 -0
- sqlspec/driver/_storage_helpers.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/driver/_storage_helpers.py +144 -0
- sqlspec/driver/_sync.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/driver/_sync.py +1710 -0
- sqlspec/exceptions.py +338 -0
- sqlspec/extensions/__init__.py +0 -0
- sqlspec/extensions/adk/__init__.py +70 -0
- sqlspec/extensions/adk/_types.py +51 -0
- sqlspec/extensions/adk/converters.py +172 -0
- sqlspec/extensions/adk/memory/__init__.py +69 -0
- sqlspec/extensions/adk/memory/_types.py +30 -0
- sqlspec/extensions/adk/memory/converters.py +149 -0
- sqlspec/extensions/adk/memory/service.py +217 -0
- sqlspec/extensions/adk/memory/store.py +569 -0
- sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +246 -0
- sqlspec/extensions/adk/migrations/__init__.py +0 -0
- sqlspec/extensions/adk/service.py +225 -0
- sqlspec/extensions/adk/store.py +567 -0
- sqlspec/extensions/events/__init__.py +51 -0
- sqlspec/extensions/events/_channel.py +703 -0
- sqlspec/extensions/events/_hints.py +45 -0
- sqlspec/extensions/events/_models.py +23 -0
- sqlspec/extensions/events/_payload.py +69 -0
- sqlspec/extensions/events/_protocols.py +134 -0
- sqlspec/extensions/events/_queue.py +461 -0
- sqlspec/extensions/events/_store.py +209 -0
- sqlspec/extensions/events/migrations/0001_create_event_queue.py +59 -0
- sqlspec/extensions/events/migrations/__init__.py +3 -0
- sqlspec/extensions/fastapi/__init__.py +19 -0
- sqlspec/extensions/fastapi/extension.py +351 -0
- sqlspec/extensions/fastapi/providers.py +607 -0
- sqlspec/extensions/flask/__init__.py +37 -0
- sqlspec/extensions/flask/_state.py +76 -0
- sqlspec/extensions/flask/_utils.py +71 -0
- sqlspec/extensions/flask/extension.py +519 -0
- sqlspec/extensions/litestar/__init__.py +28 -0
- sqlspec/extensions/litestar/_utils.py +52 -0
- sqlspec/extensions/litestar/channels.py +165 -0
- sqlspec/extensions/litestar/cli.py +102 -0
- sqlspec/extensions/litestar/config.py +90 -0
- sqlspec/extensions/litestar/handlers.py +316 -0
- sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
- sqlspec/extensions/litestar/migrations/__init__.py +3 -0
- sqlspec/extensions/litestar/plugin.py +671 -0
- sqlspec/extensions/litestar/providers.py +526 -0
- sqlspec/extensions/litestar/store.py +296 -0
- sqlspec/extensions/otel/__init__.py +58 -0
- sqlspec/extensions/prometheus/__init__.py +113 -0
- sqlspec/extensions/starlette/__init__.py +19 -0
- sqlspec/extensions/starlette/_state.py +30 -0
- sqlspec/extensions/starlette/_utils.py +96 -0
- sqlspec/extensions/starlette/extension.py +346 -0
- sqlspec/extensions/starlette/middleware.py +235 -0
- sqlspec/loader.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/loader.py +702 -0
- sqlspec/migrations/__init__.py +36 -0
- sqlspec/migrations/base.py +731 -0
- sqlspec/migrations/commands.py +1232 -0
- sqlspec/migrations/context.py +157 -0
- sqlspec/migrations/fix.py +204 -0
- sqlspec/migrations/loaders.py +443 -0
- sqlspec/migrations/runner.py +1172 -0
- sqlspec/migrations/templates.py +234 -0
- sqlspec/migrations/tracker.py +611 -0
- sqlspec/migrations/utils.py +256 -0
- sqlspec/migrations/validation.py +207 -0
- sqlspec/migrations/version.py +446 -0
- sqlspec/observability/__init__.py +55 -0
- sqlspec/observability/_common.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/observability/_common.py +77 -0
- sqlspec/observability/_config.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/observability/_config.py +348 -0
- sqlspec/observability/_diagnostics.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/observability/_diagnostics.py +74 -0
- sqlspec/observability/_dispatcher.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/observability/_dispatcher.py +152 -0
- sqlspec/observability/_formatters/__init__.py +13 -0
- sqlspec/observability/_formatters/_aws.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/observability/_formatters/_aws.py +102 -0
- sqlspec/observability/_formatters/_azure.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/observability/_formatters/_azure.py +96 -0
- sqlspec/observability/_formatters/_base.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/observability/_formatters/_base.py +57 -0
- sqlspec/observability/_formatters/_gcp.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/observability/_formatters/_gcp.py +131 -0
- sqlspec/observability/_formatting.py +58 -0
- sqlspec/observability/_observer.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/observability/_observer.py +357 -0
- sqlspec/observability/_runtime.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/observability/_runtime.py +420 -0
- sqlspec/observability/_sampling.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/observability/_sampling.py +188 -0
- sqlspec/observability/_spans.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/observability/_spans.py +161 -0
- sqlspec/protocols.py +916 -0
- sqlspec/py.typed +0 -0
- sqlspec/storage/__init__.py +48 -0
- sqlspec/storage/_utils.py +104 -0
- sqlspec/storage/backends/__init__.py +1 -0
- sqlspec/storage/backends/base.py +253 -0
- sqlspec/storage/backends/fsspec.py +529 -0
- sqlspec/storage/backends/local.py +441 -0
- sqlspec/storage/backends/obstore.py +916 -0
- sqlspec/storage/errors.py +104 -0
- sqlspec/storage/pipeline.py +582 -0
- sqlspec/storage/registry.py +301 -0
- sqlspec/typing.py +395 -0
- sqlspec/utils/__init__.py +7 -0
- sqlspec/utils/arrow_helpers.py +318 -0
- sqlspec/utils/config_tools.py +332 -0
- sqlspec/utils/correlation.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/correlation.py +134 -0
- sqlspec/utils/deprecation.py +190 -0
- sqlspec/utils/fixtures.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/fixtures.py +258 -0
- sqlspec/utils/logging.py +222 -0
- sqlspec/utils/module_loader.py +306 -0
- sqlspec/utils/portal.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/portal.py +375 -0
- sqlspec/utils/schema.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/schema.py +485 -0
- sqlspec/utils/serializers.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/serializers.py +408 -0
- sqlspec/utils/singleton.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/singleton.py +41 -0
- sqlspec/utils/sync_tools.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/sync_tools.py +311 -0
- sqlspec/utils/text.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/text.py +108 -0
- sqlspec/utils/type_converters.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/type_converters.py +128 -0
- sqlspec/utils/type_guards.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/type_guards.py +1360 -0
- sqlspec/utils/uuids.cpython-310-aarch64-linux-gnu.so +0 -0
- sqlspec/utils/uuids.py +225 -0
- sqlspec-0.36.0.dist-info/METADATA +205 -0
- sqlspec-0.36.0.dist-info/RECORD +531 -0
- sqlspec-0.36.0.dist-info/WHEEL +7 -0
- sqlspec-0.36.0.dist-info/entry_points.txt +2 -0
- sqlspec-0.36.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,821 @@
|
|
|
1
|
+
"""MERGE statement builder.
|
|
2
|
+
|
|
3
|
+
Provides a fluent interface for building SQL MERGE queries with
|
|
4
|
+
parameter binding and validation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import contextlib
|
|
8
|
+
from collections.abc import Mapping, Sequence
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from decimal import Decimal
|
|
11
|
+
from itertools import starmap
|
|
12
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
13
|
+
|
|
14
|
+
import sqlglot as sg
|
|
15
|
+
from mypy_extensions import trait
|
|
16
|
+
from sqlglot import exp
|
|
17
|
+
from sqlglot.errors import ParseError
|
|
18
|
+
from typing_extensions import Self
|
|
19
|
+
|
|
20
|
+
from sqlspec.builder._base import QueryBuilder
|
|
21
|
+
from sqlspec.builder._explain import ExplainMixin
|
|
22
|
+
from sqlspec.builder._parsing_utils import extract_sql_object_expression
|
|
23
|
+
from sqlspec.builder._select import is_explicitly_quoted
|
|
24
|
+
from sqlspec.core import SQLResult
|
|
25
|
+
from sqlspec.exceptions import DialectNotSupportedError, SQLBuilderError
|
|
26
|
+
from sqlspec.utils.serializers import to_json
|
|
27
|
+
from sqlspec.utils.type_guards import has_expression_and_sql
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from sqlglot.dialects.dialect import DialectType
|
|
31
|
+
|
|
32
|
+
__all__ = ("Merge",)
|
|
33
|
+
|
|
34
|
+
MERGE_UNSUPPORTED_DIALECTS = frozenset({"mysql", "sqlite", "duckdb"})
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@trait
|
|
38
|
+
class _MergeAssignmentMixin:
|
|
39
|
+
"""Shared assignment helpers for MERGE clause mixins."""
|
|
40
|
+
|
|
41
|
+
__slots__ = ()
|
|
42
|
+
|
|
43
|
+
def _is_column_reference(self, value: str) -> bool:
|
|
44
|
+
"""Check if value is a SQL expression rather than a literal string.
|
|
45
|
+
|
|
46
|
+
Returns True for qualified column references, SQL keywords, functions, and expressions.
|
|
47
|
+
Returns False for plain literal strings that should be parameterized.
|
|
48
|
+
"""
|
|
49
|
+
if not isinstance(value, str):
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
builder = cast("QueryBuilder", self)
|
|
53
|
+
with contextlib.suppress(ParseError):
|
|
54
|
+
parsed: exp.Expression | None = exp.maybe_parse(value.strip(), dialect=builder.dialect)
|
|
55
|
+
if parsed is None:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
if isinstance(parsed, exp.Column):
|
|
59
|
+
return parsed.table is not None and bool(parsed.table)
|
|
60
|
+
|
|
61
|
+
return isinstance(
|
|
62
|
+
parsed,
|
|
63
|
+
(
|
|
64
|
+
exp.Dot,
|
|
65
|
+
exp.Add,
|
|
66
|
+
exp.Sub,
|
|
67
|
+
exp.Mul,
|
|
68
|
+
exp.Div,
|
|
69
|
+
exp.Mod,
|
|
70
|
+
exp.Func,
|
|
71
|
+
exp.Anonymous,
|
|
72
|
+
exp.Null,
|
|
73
|
+
exp.CurrentTimestamp,
|
|
74
|
+
exp.CurrentDate,
|
|
75
|
+
exp.CurrentTime,
|
|
76
|
+
exp.Paren,
|
|
77
|
+
exp.Case,
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
def _process_assignment(self, target_column: str, value: Any) -> exp.Expression:
|
|
83
|
+
column_identifier = exp.column(target_column) if isinstance(target_column, str) else target_column
|
|
84
|
+
|
|
85
|
+
if has_expression_and_sql(value):
|
|
86
|
+
value_expr = extract_sql_object_expression(value, builder=self)
|
|
87
|
+
return exp.EQ(this=column_identifier, expression=value_expr)
|
|
88
|
+
if isinstance(value, exp.Expression):
|
|
89
|
+
return exp.EQ(this=column_identifier, expression=value)
|
|
90
|
+
if isinstance(value, str) and self._is_column_reference(value):
|
|
91
|
+
builder = cast("QueryBuilder", self)
|
|
92
|
+
parsed_expression: exp.Expression | None = exp.maybe_parse(value, dialect=builder.dialect)
|
|
93
|
+
if parsed_expression is None:
|
|
94
|
+
msg = f"Could not parse assignment expression: {value}"
|
|
95
|
+
raise SQLBuilderError(msg)
|
|
96
|
+
return exp.EQ(this=column_identifier, expression=parsed_expression)
|
|
97
|
+
|
|
98
|
+
column_name = target_column if isinstance(target_column, str) else str(target_column)
|
|
99
|
+
column_leaf = column_name.split(".")[-1]
|
|
100
|
+
placeholder, _ = cast("QueryBuilder", self).create_placeholder(value, column_leaf)
|
|
101
|
+
return exp.EQ(this=column_identifier, expression=placeholder)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@trait
|
|
105
|
+
class MergeIntoClauseMixin:
|
|
106
|
+
"""Mixin providing INTO clause for MERGE builders."""
|
|
107
|
+
|
|
108
|
+
__slots__ = ()
|
|
109
|
+
|
|
110
|
+
def get_expression(self) -> exp.Expression | None: ...
|
|
111
|
+
def set_expression(self, expression: exp.Expression) -> None: ...
|
|
112
|
+
|
|
113
|
+
def into(self, table: str | exp.Expression, alias: str | None = None) -> Self:
|
|
114
|
+
current_expr = self.get_expression()
|
|
115
|
+
if current_expr is None or not isinstance(current_expr, exp.Merge):
|
|
116
|
+
self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
|
|
117
|
+
current_expr = self.get_expression()
|
|
118
|
+
|
|
119
|
+
assert current_expr is not None
|
|
120
|
+
|
|
121
|
+
table_expr: exp.Expression
|
|
122
|
+
if isinstance(table, str):
|
|
123
|
+
table_expr = exp.to_table(table)
|
|
124
|
+
if is_explicitly_quoted(table):
|
|
125
|
+
stripped = table.strip('"`')
|
|
126
|
+
table_expr.set("quoted", True)
|
|
127
|
+
table_expr.set("this", exp.to_identifier(stripped, quoted=True))
|
|
128
|
+
cast("QueryBuilder", self)._merge_target_quoted = True # pyright: ignore[reportPrivateUsage]
|
|
129
|
+
else:
|
|
130
|
+
cast("QueryBuilder", self)._merge_target_quoted = False # pyright: ignore[reportPrivateUsage]
|
|
131
|
+
if alias:
|
|
132
|
+
table_expr = exp.alias_(table_expr, alias, table=True)
|
|
133
|
+
else:
|
|
134
|
+
table_expr = table
|
|
135
|
+
|
|
136
|
+
current_expr.set("this", table_expr)
|
|
137
|
+
return self
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@trait
|
|
141
|
+
class MergeUsingClauseMixin(_MergeAssignmentMixin):
|
|
142
|
+
"""Mixin providing USING clause for MERGE builders."""
|
|
143
|
+
|
|
144
|
+
__slots__ = ()
|
|
145
|
+
|
|
146
|
+
def get_expression(self) -> exp.Expression | None: ...
|
|
147
|
+
def set_expression(self, expression: exp.Expression) -> None: ...
|
|
148
|
+
|
|
149
|
+
def _create_dict_source_expression(
|
|
150
|
+
self, source: "dict[str, Any] | list[dict[str, Any]]", alias: "str | None"
|
|
151
|
+
) -> "exp.Expression":
|
|
152
|
+
"""Create USING clause expression from dict or list of dicts.
|
|
153
|
+
|
|
154
|
+
Uses JSON-based approach for type-safe bulk operations:
|
|
155
|
+
- PostgreSQL: json_populate_recordset(NULL::table_name, $1::jsonb)
|
|
156
|
+
- Oracle: JSON_TABLE(:payload, '$[*]' COLUMNS (...))
|
|
157
|
+
- Others: Fall back to SELECT with parameterized values
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
source: Dict or list of dicts for USING clause
|
|
161
|
+
alias: Optional alias for the source
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Expression for USING clause
|
|
165
|
+
"""
|
|
166
|
+
data: list[dict[str, Any]]
|
|
167
|
+
is_list: bool
|
|
168
|
+
if isinstance(source, list):
|
|
169
|
+
data = source
|
|
170
|
+
is_list = True
|
|
171
|
+
else:
|
|
172
|
+
data = [source]
|
|
173
|
+
is_list = False
|
|
174
|
+
|
|
175
|
+
if not data:
|
|
176
|
+
msg = "Cannot create USING clause from empty list"
|
|
177
|
+
raise SQLBuilderError(msg)
|
|
178
|
+
|
|
179
|
+
columns = list(data[0].keys())
|
|
180
|
+
builder = cast("QueryBuilder", self)
|
|
181
|
+
dialect = builder.dialect_name
|
|
182
|
+
|
|
183
|
+
if dialect == "postgres":
|
|
184
|
+
return self._create_postgres_json_source(data, columns, is_list, alias)
|
|
185
|
+
if dialect == "oracle":
|
|
186
|
+
return self._create_oracle_json_source(data, columns, alias)
|
|
187
|
+
|
|
188
|
+
return self._create_select_union_source(data, columns, is_list, alias)
|
|
189
|
+
|
|
190
|
+
def _create_postgres_json_source(
|
|
191
|
+
self, data: "list[dict[str, Any]]", columns: "list[str]", is_list: bool, alias: "str | None"
|
|
192
|
+
) -> "exp.Expression":
|
|
193
|
+
"""Create PostgreSQL jsonb_to_recordset source with explicit column definitions.
|
|
194
|
+
|
|
195
|
+
Uses jsonb_to_recordset(jsonb) AS alias(col1 type1, col2 type2, ...) pattern
|
|
196
|
+
which avoids composite type dependencies and provides explicit type definitions.
|
|
197
|
+
|
|
198
|
+
Passes native Python list to driver for JSON serialization via driver-specific
|
|
199
|
+
mechanisms (AsyncPG json_serializer, Psycopg/Psqlpy JSON codecs).
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
json_value = data if is_list else [data[0]]
|
|
203
|
+
_, json_param_name = cast("QueryBuilder", self).create_placeholder(json_value, "json_data")
|
|
204
|
+
|
|
205
|
+
sample_values: dict[str, Any] = {}
|
|
206
|
+
for record in data:
|
|
207
|
+
for column, value in record.items():
|
|
208
|
+
if value is not None and column not in sample_values:
|
|
209
|
+
sample_values[column] = value
|
|
210
|
+
|
|
211
|
+
alias_name = alias or "src"
|
|
212
|
+
recordset_alias = f"{alias_name}_data"
|
|
213
|
+
|
|
214
|
+
column_type_spec = ", ".join([f"{col} {self._infer_postgres_type(sample_values.get(col))}" for col in columns])
|
|
215
|
+
column_selects = ", ".join(columns)
|
|
216
|
+
from_sql = (
|
|
217
|
+
f"SELECT {column_selects} FROM jsonb_to_recordset(:{json_param_name}::jsonb) AS "
|
|
218
|
+
f"{recordset_alias}({column_type_spec})"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
parsed = sg.parse_one(from_sql, dialect="postgres")
|
|
222
|
+
return exp.Subquery(
|
|
223
|
+
this=parsed,
|
|
224
|
+
alias=exp.TableAlias(
|
|
225
|
+
this=exp.to_identifier(alias_name), columns=[exp.to_identifier(col) for col in columns]
|
|
226
|
+
),
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def _create_oracle_json_source(
|
|
230
|
+
self, data: "list[dict[str, Any]]", columns: "list[str]", alias: "str | None"
|
|
231
|
+
) -> "exp.Expression":
|
|
232
|
+
"""Create Oracle JSON_TABLE source (production-proven pattern from oracledb-vertexai-demo)."""
|
|
233
|
+
json_value = to_json(data)
|
|
234
|
+
_, json_param_name = cast("QueryBuilder", self).create_placeholder(json_value, "json_payload")
|
|
235
|
+
|
|
236
|
+
sample_values: dict[str, Any] = {}
|
|
237
|
+
for record in data:
|
|
238
|
+
for column, value in record.items():
|
|
239
|
+
if value is not None and column not in sample_values:
|
|
240
|
+
sample_values[column] = value
|
|
241
|
+
|
|
242
|
+
json_columns = [
|
|
243
|
+
f"{column} {self._infer_oracle_type(sample_values.get(column))} PATH '$.{column}'" for column in columns
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
alias_name = alias or "src"
|
|
247
|
+
column_selects = ", ".join(columns)
|
|
248
|
+
columns_clause = ", ".join(json_columns)
|
|
249
|
+
|
|
250
|
+
from_sql = f"SELECT {column_selects} FROM JSON_TABLE(:{json_param_name}, '$[*]' COLUMNS ({columns_clause}))"
|
|
251
|
+
|
|
252
|
+
parsed = sg.parse_one(from_sql, dialect="oracle")
|
|
253
|
+
return exp.Subquery(this=parsed, alias=exp.TableAlias(this=exp.to_identifier(alias_name)))
|
|
254
|
+
|
|
255
|
+
def _infer_postgres_type(self, value: "Any") -> str:
|
|
256
|
+
"""Infer PostgreSQL column type from Python value.
|
|
257
|
+
|
|
258
|
+
Maps Python types to PostgreSQL types for jsonb_to_recordset column definitions.
|
|
259
|
+
|
|
260
|
+
Note: When value is None and we cannot infer the type from other records,
|
|
261
|
+
we default to NUMERIC which is more permissive than TEXT for NULL values
|
|
262
|
+
and commonly used for business data.
|
|
263
|
+
"""
|
|
264
|
+
if value is None:
|
|
265
|
+
return "NUMERIC"
|
|
266
|
+
if isinstance(value, bool):
|
|
267
|
+
return "BOOLEAN"
|
|
268
|
+
if isinstance(value, int):
|
|
269
|
+
return "INTEGER"
|
|
270
|
+
if isinstance(value, float):
|
|
271
|
+
return "DOUBLE PRECISION"
|
|
272
|
+
if isinstance(value, Decimal):
|
|
273
|
+
return "NUMERIC"
|
|
274
|
+
if isinstance(value, (dict, list)):
|
|
275
|
+
return "JSONB"
|
|
276
|
+
if isinstance(value, datetime):
|
|
277
|
+
return "TIMESTAMP"
|
|
278
|
+
return "TEXT"
|
|
279
|
+
|
|
280
|
+
def _infer_oracle_type(self, value: "Any") -> str:
|
|
281
|
+
"""Infer Oracle column type for JSON_TABLE projection."""
|
|
282
|
+
varchar2_max = 4000
|
|
283
|
+
|
|
284
|
+
if isinstance(value, bool):
|
|
285
|
+
return "NUMBER(1)"
|
|
286
|
+
if isinstance(value, (int, float, Decimal)):
|
|
287
|
+
return "NUMBER"
|
|
288
|
+
if isinstance(value, (dict, list)):
|
|
289
|
+
return "JSON"
|
|
290
|
+
if isinstance(value, datetime):
|
|
291
|
+
return "TIMESTAMP"
|
|
292
|
+
if value is not None and len(str(value)) > varchar2_max:
|
|
293
|
+
return "CLOB"
|
|
294
|
+
return f"VARCHAR2({varchar2_max})"
|
|
295
|
+
|
|
296
|
+
def _create_select_union_source(
|
|
297
|
+
self, data: "list[dict[str, Any]]", columns: "list[str]", is_list: bool, alias: "str | None"
|
|
298
|
+
) -> "exp.Expression":
|
|
299
|
+
"""Create fallback SELECT UNION source for other databases."""
|
|
300
|
+
parameterized_values: list[list[exp.Expression]] = []
|
|
301
|
+
for row in data:
|
|
302
|
+
row_params: list[exp.Expression] = []
|
|
303
|
+
for column in columns:
|
|
304
|
+
value = row.get(column)
|
|
305
|
+
column_name = column if isinstance(column, str) else str(column)
|
|
306
|
+
if "." in column_name:
|
|
307
|
+
column_name = column_name.split(".")[-1]
|
|
308
|
+
placeholder, _ = cast("QueryBuilder", self).create_placeholder(value, column_name)
|
|
309
|
+
row_params.append(placeholder)
|
|
310
|
+
parameterized_values.append(row_params)
|
|
311
|
+
|
|
312
|
+
if is_list:
|
|
313
|
+
union_selects: list[exp.Select] = []
|
|
314
|
+
for row_params in parameterized_values:
|
|
315
|
+
select_expr = exp.Select()
|
|
316
|
+
select_expr.set(
|
|
317
|
+
"expressions", [exp.alias_(row_params[index], column) for index, column in enumerate(columns)]
|
|
318
|
+
)
|
|
319
|
+
union_selects.append(select_expr)
|
|
320
|
+
|
|
321
|
+
source_expr: exp.Expression
|
|
322
|
+
if len(union_selects) == 1:
|
|
323
|
+
source_expr = union_selects[0]
|
|
324
|
+
else:
|
|
325
|
+
union_expr: exp.Expression = union_selects[0]
|
|
326
|
+
for select in union_selects[1:]:
|
|
327
|
+
union_expr = exp.Union(this=union_expr, expression=select, distinct=False)
|
|
328
|
+
source_expr = union_expr
|
|
329
|
+
|
|
330
|
+
return exp.paren(source_expr)
|
|
331
|
+
|
|
332
|
+
select_expr = exp.Select()
|
|
333
|
+
select_expr.set(
|
|
334
|
+
"expressions", [exp.alias_(parameterized_values[0][index], column) for index, column in enumerate(columns)]
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
return exp.paren(select_expr)
|
|
338
|
+
|
|
339
|
+
def using(self, source: str | exp.Expression | Any, alias: str | None = None) -> Self:
|
|
340
|
+
current_expr = self.get_expression()
|
|
341
|
+
if current_expr is None or not isinstance(current_expr, exp.Merge):
|
|
342
|
+
self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
|
|
343
|
+
current_expr = self.get_expression()
|
|
344
|
+
|
|
345
|
+
assert current_expr is not None
|
|
346
|
+
source_expr: exp.Expression
|
|
347
|
+
if isinstance(source, str):
|
|
348
|
+
source_expr = exp.to_table(source, alias=alias)
|
|
349
|
+
elif isinstance(source, (dict, list)):
|
|
350
|
+
paren_expr = self._create_dict_source_expression(source, alias)
|
|
351
|
+
if alias and isinstance(paren_expr, exp.Paren):
|
|
352
|
+
source_expr = exp.Subquery(this=paren_expr.this, alias=exp.to_identifier(alias))
|
|
353
|
+
else:
|
|
354
|
+
source_expr = paren_expr
|
|
355
|
+
elif isinstance(source, QueryBuilder):
|
|
356
|
+
builder = cast("QueryBuilder", self)
|
|
357
|
+
for param_name, param_value in source.parameters.items():
|
|
358
|
+
builder.add_parameter(param_value, name=param_name)
|
|
359
|
+
subquery_expression_source = source.get_expression()
|
|
360
|
+
if not isinstance(subquery_expression_source, exp.Expression):
|
|
361
|
+
subquery_expression_source = exp.select()
|
|
362
|
+
|
|
363
|
+
if alias:
|
|
364
|
+
source_expr = exp.Subquery(this=subquery_expression_source, alias=exp.to_identifier(alias))
|
|
365
|
+
else:
|
|
366
|
+
source_expr = exp.paren(subquery_expression_source)
|
|
367
|
+
elif isinstance(source, exp.Expression):
|
|
368
|
+
# Handle different expression types for MERGE USING
|
|
369
|
+
if isinstance(source, exp.Select):
|
|
370
|
+
# Wrap SELECT in Subquery if alias provided
|
|
371
|
+
source_expr = exp.Subquery(this=source, alias=exp.to_identifier(alias)) if alias else exp.paren(source)
|
|
372
|
+
elif isinstance(source, exp.Paren) and alias:
|
|
373
|
+
# Convert Paren to Subquery with alias
|
|
374
|
+
inner = source.this
|
|
375
|
+
source_expr = exp.Subquery(this=inner, alias=exp.to_identifier(alias))
|
|
376
|
+
elif isinstance(source, exp.Subquery) and alias:
|
|
377
|
+
# Update existing Subquery's alias
|
|
378
|
+
source.set("alias", exp.to_identifier(alias))
|
|
379
|
+
source_expr = source
|
|
380
|
+
else:
|
|
381
|
+
# Table name or other expression - use standard aliasing
|
|
382
|
+
source_expr = exp.alias_(source, alias) if alias else source
|
|
383
|
+
else:
|
|
384
|
+
msg = f"Unsupported source type for USING clause: {type(source)}"
|
|
385
|
+
raise SQLBuilderError(msg)
|
|
386
|
+
|
|
387
|
+
current_expr.set("using", source_expr)
|
|
388
|
+
return self
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
@trait
|
|
392
|
+
class MergeOnClauseMixin:
|
|
393
|
+
"""Mixin providing ON clause for MERGE builders."""
|
|
394
|
+
|
|
395
|
+
__slots__ = ()
|
|
396
|
+
|
|
397
|
+
def get_expression(self) -> exp.Expression | None: ...
|
|
398
|
+
def set_expression(self, expression: exp.Expression) -> None: ...
|
|
399
|
+
|
|
400
|
+
def on(self, condition: str | exp.Expression) -> Self:
|
|
401
|
+
current_expr = self.get_expression()
|
|
402
|
+
if current_expr is None or not isinstance(current_expr, exp.Merge):
|
|
403
|
+
self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
|
|
404
|
+
current_expr = self.get_expression()
|
|
405
|
+
|
|
406
|
+
assert current_expr is not None
|
|
407
|
+
if isinstance(condition, str):
|
|
408
|
+
builder = cast("QueryBuilder", self)
|
|
409
|
+
parsed_condition: exp.Expression | None = exp.maybe_parse(condition, dialect=builder.dialect)
|
|
410
|
+
if parsed_condition is None:
|
|
411
|
+
msg = f"Could not parse ON condition: {condition}"
|
|
412
|
+
raise SQLBuilderError(msg)
|
|
413
|
+
condition_expr = parsed_condition
|
|
414
|
+
elif isinstance(condition, exp.Expression):
|
|
415
|
+
condition_expr = condition
|
|
416
|
+
else:
|
|
417
|
+
msg = f"Unsupported condition type for ON clause: {type(condition)}"
|
|
418
|
+
raise SQLBuilderError(msg)
|
|
419
|
+
|
|
420
|
+
current_expr.set("on", exp.paren(condition_expr))
|
|
421
|
+
return self
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
@trait
|
|
425
|
+
class MergeMatchedClauseMixin(_MergeAssignmentMixin):
|
|
426
|
+
"""Mixin providing WHEN MATCHED THEN ... clauses for MERGE builders."""
|
|
427
|
+
|
|
428
|
+
__slots__ = ()
|
|
429
|
+
|
|
430
|
+
def get_expression(self) -> exp.Expression | None: ...
|
|
431
|
+
def set_expression(self, expression: exp.Expression) -> None: ...
|
|
432
|
+
|
|
433
|
+
def when_matched_then_update(
|
|
434
|
+
self,
|
|
435
|
+
set_values: dict[str, Any] | None = None,
|
|
436
|
+
condition: str | exp.Expression | None = None,
|
|
437
|
+
**assignments: Any,
|
|
438
|
+
) -> Self:
|
|
439
|
+
current_expr = self.get_expression()
|
|
440
|
+
if current_expr is None or not isinstance(current_expr, exp.Merge):
|
|
441
|
+
self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
|
|
442
|
+
current_expr = self.get_expression()
|
|
443
|
+
|
|
444
|
+
assert current_expr is not None
|
|
445
|
+
combined_assignments: dict[str, Any] = {}
|
|
446
|
+
if set_values:
|
|
447
|
+
combined_assignments.update(set_values)
|
|
448
|
+
if assignments:
|
|
449
|
+
combined_assignments.update(assignments)
|
|
450
|
+
|
|
451
|
+
if not combined_assignments:
|
|
452
|
+
msg = "No update values provided. Use set_values or keyword arguments."
|
|
453
|
+
raise SQLBuilderError(msg)
|
|
454
|
+
|
|
455
|
+
set_expressions = list(starmap(self._process_assignment, combined_assignments.items()))
|
|
456
|
+
update_expression = exp.Update(expressions=set_expressions)
|
|
457
|
+
|
|
458
|
+
when_kwargs: dict[str, Any] = {"matched": True, "then": update_expression}
|
|
459
|
+
if condition is not None:
|
|
460
|
+
if isinstance(condition, str):
|
|
461
|
+
builder = cast("QueryBuilder", self)
|
|
462
|
+
parsed_condition: exp.Expression | None = exp.maybe_parse(condition, dialect=builder.dialect)
|
|
463
|
+
if parsed_condition is None:
|
|
464
|
+
msg = f"Could not parse WHEN clause condition: {condition}"
|
|
465
|
+
raise SQLBuilderError(msg)
|
|
466
|
+
condition_expr = parsed_condition
|
|
467
|
+
elif isinstance(condition, exp.Expression):
|
|
468
|
+
condition_expr = condition
|
|
469
|
+
else:
|
|
470
|
+
msg = f"Unsupported condition type for WHEN clause: {type(condition)}"
|
|
471
|
+
raise SQLBuilderError(msg)
|
|
472
|
+
|
|
473
|
+
builder = cast("QueryBuilder", self)
|
|
474
|
+
dialect_name = builder.dialect_name
|
|
475
|
+
if dialect_name == "oracle":
|
|
476
|
+
update_expression.set("where", condition_expr)
|
|
477
|
+
else:
|
|
478
|
+
when_kwargs["condition"] = condition_expr
|
|
479
|
+
|
|
480
|
+
whens = current_expr.args.get("whens")
|
|
481
|
+
if not isinstance(whens, exp.Whens):
|
|
482
|
+
whens = exp.Whens(expressions=[])
|
|
483
|
+
current_expr.set("whens", whens)
|
|
484
|
+
whens.append("expressions", exp.When(**when_kwargs))
|
|
485
|
+
return self
|
|
486
|
+
|
|
487
|
+
def when_matched_then_delete(self, condition: str | exp.Expression | None = None) -> Self:
|
|
488
|
+
current_expr = self.get_expression()
|
|
489
|
+
if current_expr is None or not isinstance(current_expr, exp.Merge):
|
|
490
|
+
self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
|
|
491
|
+
current_expr = self.get_expression()
|
|
492
|
+
|
|
493
|
+
assert current_expr is not None
|
|
494
|
+
when_kwargs: dict[str, Any] = {"matched": True, "then": exp.Var(this="DELETE")}
|
|
495
|
+
if condition is not None:
|
|
496
|
+
if isinstance(condition, str):
|
|
497
|
+
builder = cast("QueryBuilder", self)
|
|
498
|
+
parsed_condition: exp.Expression | None = exp.maybe_parse(condition, dialect=builder.dialect)
|
|
499
|
+
if parsed_condition is None:
|
|
500
|
+
msg = f"Could not parse WHEN clause condition: {condition}"
|
|
501
|
+
raise SQLBuilderError(msg)
|
|
502
|
+
when_kwargs["condition"] = parsed_condition
|
|
503
|
+
elif isinstance(condition, exp.Expression):
|
|
504
|
+
when_kwargs["condition"] = condition
|
|
505
|
+
else:
|
|
506
|
+
msg = f"Unsupported condition type for WHEN clause: {type(condition)}"
|
|
507
|
+
raise SQLBuilderError(msg)
|
|
508
|
+
|
|
509
|
+
whens = current_expr.args.get("whens")
|
|
510
|
+
if not isinstance(whens, exp.Whens):
|
|
511
|
+
whens = exp.Whens(expressions=[])
|
|
512
|
+
current_expr.set("whens", whens)
|
|
513
|
+
whens.append("expressions", exp.When(**when_kwargs))
|
|
514
|
+
return self
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
@trait
|
|
518
|
+
class MergeNotMatchedClauseMixin(_MergeAssignmentMixin):
|
|
519
|
+
"""Mixin providing WHEN NOT MATCHED THEN ... clauses for MERGE builders."""
|
|
520
|
+
|
|
521
|
+
__slots__ = ()
|
|
522
|
+
|
|
523
|
+
def get_expression(self) -> exp.Expression | None: ...
|
|
524
|
+
def set_expression(self, expression: exp.Expression) -> None: ...
|
|
525
|
+
|
|
526
|
+
def when_not_matched_then_insert(
|
|
527
|
+
self,
|
|
528
|
+
columns: Mapping[str, Any] | Sequence[str] | None = None,
|
|
529
|
+
values: Sequence[Any] | None = None,
|
|
530
|
+
**value_kwargs: Any,
|
|
531
|
+
) -> Self:
|
|
532
|
+
current_expr = self.get_expression()
|
|
533
|
+
if current_expr is None or not isinstance(current_expr, exp.Merge):
|
|
534
|
+
self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
|
|
535
|
+
current_expr = self.get_expression()
|
|
536
|
+
|
|
537
|
+
assert current_expr is not None
|
|
538
|
+
insert_expr = exp.Insert()
|
|
539
|
+
column_names: list[str]
|
|
540
|
+
column_values: list[Any]
|
|
541
|
+
|
|
542
|
+
if isinstance(columns, Mapping):
|
|
543
|
+
combined = dict(columns)
|
|
544
|
+
if value_kwargs:
|
|
545
|
+
combined.update(value_kwargs)
|
|
546
|
+
column_names = list(combined.keys())
|
|
547
|
+
column_values = list(combined.values())
|
|
548
|
+
elif value_kwargs:
|
|
549
|
+
column_names = list(value_kwargs.keys())
|
|
550
|
+
column_values = list(value_kwargs.values())
|
|
551
|
+
else:
|
|
552
|
+
if columns is None:
|
|
553
|
+
msg = "Columns must be provided when not using keyword arguments."
|
|
554
|
+
raise SQLBuilderError(msg)
|
|
555
|
+
column_names = [str(column) for column in columns]
|
|
556
|
+
if values is None:
|
|
557
|
+
using_alias = None
|
|
558
|
+
using_expr = current_expr.args.get("using")
|
|
559
|
+
if using_expr is not None and isinstance(using_expr, (exp.Subquery, exp.Table)):
|
|
560
|
+
using_alias = using_expr.alias
|
|
561
|
+
elif using_expr is not None:
|
|
562
|
+
try:
|
|
563
|
+
using_alias = using_expr.alias
|
|
564
|
+
except AttributeError:
|
|
565
|
+
using_alias = None
|
|
566
|
+
column_values = [f"{using_alias}.{col}" for col in column_names] if using_alias else column_names
|
|
567
|
+
else:
|
|
568
|
+
column_values = list(values)
|
|
569
|
+
if len(column_names) != len(column_values):
|
|
570
|
+
msg = "Number of columns must match number of values for MERGE insert"
|
|
571
|
+
raise SQLBuilderError(msg)
|
|
572
|
+
|
|
573
|
+
insert_columns = [exp.column(name) for name in column_names]
|
|
574
|
+
|
|
575
|
+
insert_values: list[exp.Expression] = []
|
|
576
|
+
for column_name, value in zip(column_names, column_values, strict=True):
|
|
577
|
+
if has_expression_and_sql(value):
|
|
578
|
+
insert_values.append(extract_sql_object_expression(value, builder=self))
|
|
579
|
+
elif isinstance(value, exp.Expression):
|
|
580
|
+
insert_values.append(value)
|
|
581
|
+
elif isinstance(value, str):
|
|
582
|
+
if self._is_column_reference(value):
|
|
583
|
+
builder = cast("QueryBuilder", self)
|
|
584
|
+
parsed_value: exp.Expression | None = exp.maybe_parse(value, dialect=builder.dialect)
|
|
585
|
+
if parsed_value is None:
|
|
586
|
+
msg = f"Could not parse column reference: {value}"
|
|
587
|
+
raise SQLBuilderError(msg)
|
|
588
|
+
insert_values.append(parsed_value)
|
|
589
|
+
else:
|
|
590
|
+
placeholder, _ = cast("QueryBuilder", self).create_placeholder(value, column_name.split(".")[-1])
|
|
591
|
+
insert_values.append(placeholder)
|
|
592
|
+
else:
|
|
593
|
+
placeholder, _ = cast("QueryBuilder", self).create_placeholder(value, column_name.split(".")[-1])
|
|
594
|
+
insert_values.append(placeholder)
|
|
595
|
+
|
|
596
|
+
insert_expr.set("this", exp.Tuple(expressions=insert_columns))
|
|
597
|
+
insert_expr.set("expression", exp.Tuple(expressions=insert_values))
|
|
598
|
+
whens = current_expr.args.get("whens")
|
|
599
|
+
if not isinstance(whens, exp.Whens):
|
|
600
|
+
whens = exp.Whens(expressions=[])
|
|
601
|
+
current_expr.set("whens", whens)
|
|
602
|
+
whens.append("expressions", exp.When(matched=False, then=insert_expr))
|
|
603
|
+
return self
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
@trait
|
|
607
|
+
class MergeNotMatchedBySourceClauseMixin(_MergeAssignmentMixin):
|
|
608
|
+
"""Mixin providing WHEN NOT MATCHED BY SOURCE THEN ... clauses."""
|
|
609
|
+
|
|
610
|
+
__slots__ = ()
|
|
611
|
+
|
|
612
|
+
def get_expression(self) -> exp.Expression | None: ...
|
|
613
|
+
def set_expression(self, expression: exp.Expression) -> None: ...
|
|
614
|
+
|
|
615
|
+
def when_not_matched_by_source_then_update(
|
|
616
|
+
self, set_values: dict[str, Any] | None = None, **assignments: Any
|
|
617
|
+
) -> Self:
|
|
618
|
+
current_expr = self.get_expression()
|
|
619
|
+
if current_expr is None or not isinstance(current_expr, exp.Merge):
|
|
620
|
+
self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
|
|
621
|
+
current_expr = self.get_expression()
|
|
622
|
+
|
|
623
|
+
assert current_expr is not None
|
|
624
|
+
combined_assignments: dict[str, Any] = {}
|
|
625
|
+
if set_values:
|
|
626
|
+
combined_assignments.update(set_values)
|
|
627
|
+
if assignments:
|
|
628
|
+
combined_assignments.update(assignments)
|
|
629
|
+
|
|
630
|
+
if not combined_assignments:
|
|
631
|
+
msg = "No update values provided. Use set_values or keyword arguments."
|
|
632
|
+
raise SQLBuilderError(msg)
|
|
633
|
+
|
|
634
|
+
set_expressions: list[exp.Expression] = []
|
|
635
|
+
for column_name, value in combined_assignments.items():
|
|
636
|
+
column_identifier = exp.column(column_name)
|
|
637
|
+
if has_expression_and_sql(value):
|
|
638
|
+
value_expr = extract_sql_object_expression(value, builder=self)
|
|
639
|
+
elif isinstance(value, exp.Expression):
|
|
640
|
+
value_expr = value
|
|
641
|
+
elif isinstance(value, str) and self._is_column_reference(value):
|
|
642
|
+
builder = cast("QueryBuilder", self)
|
|
643
|
+
parsed_value: exp.Expression | None = exp.maybe_parse(value, dialect=builder.dialect)
|
|
644
|
+
if parsed_value is None:
|
|
645
|
+
msg = f"Could not parse assignment expression: {value}"
|
|
646
|
+
raise SQLBuilderError(msg)
|
|
647
|
+
value_expr = parsed_value
|
|
648
|
+
else:
|
|
649
|
+
placeholder, _ = cast("QueryBuilder", self).create_placeholder(value, column_name)
|
|
650
|
+
value_expr = placeholder
|
|
651
|
+
set_expressions.append(exp.EQ(this=column_identifier, expression=value_expr))
|
|
652
|
+
|
|
653
|
+
update_expr = exp.Update(expressions=set_expressions)
|
|
654
|
+
whens = current_expr.args.get("whens")
|
|
655
|
+
if not isinstance(whens, exp.Whens):
|
|
656
|
+
whens = exp.Whens(expressions=[])
|
|
657
|
+
current_expr.set("whens", whens)
|
|
658
|
+
whens.append("expressions", exp.When(matched=False, source=True, then=update_expr))
|
|
659
|
+
return self
|
|
660
|
+
|
|
661
|
+
def when_not_matched_by_source_then_delete(self) -> Self:
|
|
662
|
+
current_expr = self.get_expression()
|
|
663
|
+
if current_expr is None or not isinstance(current_expr, exp.Merge):
|
|
664
|
+
self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
|
|
665
|
+
current_expr = self.get_expression()
|
|
666
|
+
|
|
667
|
+
assert current_expr is not None
|
|
668
|
+
whens = current_expr.args.get("whens")
|
|
669
|
+
if not isinstance(whens, exp.Whens):
|
|
670
|
+
whens = exp.Whens(expressions=[])
|
|
671
|
+
current_expr.set("whens", whens)
|
|
672
|
+
whens.append("expressions", exp.When(matched=False, source=True, then=exp.Delete()))
|
|
673
|
+
return self
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
class Merge(
|
|
677
|
+
QueryBuilder,
|
|
678
|
+
MergeUsingClauseMixin,
|
|
679
|
+
MergeOnClauseMixin,
|
|
680
|
+
MergeMatchedClauseMixin,
|
|
681
|
+
MergeNotMatchedClauseMixin,
|
|
682
|
+
MergeIntoClauseMixin,
|
|
683
|
+
MergeNotMatchedBySourceClauseMixin,
|
|
684
|
+
ExplainMixin,
|
|
685
|
+
):
|
|
686
|
+
"""Builder for MERGE statements.
|
|
687
|
+
|
|
688
|
+
Constructs SQL MERGE statements (also known as UPSERT in some databases)
|
|
689
|
+
with parameter binding and validation.
|
|
690
|
+
"""
|
|
691
|
+
|
|
692
|
+
__slots__ = ()
|
|
693
|
+
_expression: exp.Expression | None
|
|
694
|
+
_merge_target_quoted: bool
|
|
695
|
+
_lock_targets_quoted: bool
|
|
696
|
+
|
|
697
|
+
def __init__(self, target_table: str | None = None, **kwargs: Any) -> None:
|
|
698
|
+
"""Initialize MERGE with optional target table.
|
|
699
|
+
|
|
700
|
+
Args:
|
|
701
|
+
target_table: Target table name
|
|
702
|
+
**kwargs: Additional QueryBuilder arguments
|
|
703
|
+
"""
|
|
704
|
+
if "enable_optimization" not in kwargs:
|
|
705
|
+
kwargs["enable_optimization"] = False
|
|
706
|
+
(dialect, schema, enable_optimization, optimize_joins, optimize_predicates, simplify_expressions) = (
|
|
707
|
+
self._parse_query_builder_kwargs(kwargs)
|
|
708
|
+
)
|
|
709
|
+
super().__init__(
|
|
710
|
+
dialect=dialect,
|
|
711
|
+
schema=schema,
|
|
712
|
+
enable_optimization=enable_optimization,
|
|
713
|
+
optimize_joins=optimize_joins,
|
|
714
|
+
optimize_predicates=optimize_predicates,
|
|
715
|
+
simplify_expressions=simplify_expressions,
|
|
716
|
+
)
|
|
717
|
+
self._merge_target_quoted = False
|
|
718
|
+
self._lock_targets_quoted = False
|
|
719
|
+
self._initialize_expression()
|
|
720
|
+
|
|
721
|
+
if target_table:
|
|
722
|
+
self.into(target_table)
|
|
723
|
+
|
|
724
|
+
@property
|
|
725
|
+
def _expected_result_type(self) -> "type[SQLResult]":
|
|
726
|
+
"""Return the expected result type for this builder.
|
|
727
|
+
|
|
728
|
+
Returns:
|
|
729
|
+
The SQLResult type for MERGE statements.
|
|
730
|
+
"""
|
|
731
|
+
return SQLResult
|
|
732
|
+
|
|
733
|
+
def _create_base_expression(self) -> "exp.Merge":
|
|
734
|
+
"""Create a base MERGE expression.
|
|
735
|
+
|
|
736
|
+
Returns:
|
|
737
|
+
A new sqlglot Merge expression with empty clauses.
|
|
738
|
+
"""
|
|
739
|
+
return exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
|
|
740
|
+
|
|
741
|
+
def _validate_dialect_support(self) -> None:
|
|
742
|
+
"""Validate that the current dialect supports MERGE statements.
|
|
743
|
+
|
|
744
|
+
Raises:
|
|
745
|
+
DialectNotSupportedError: If the dialect does not support MERGE.
|
|
746
|
+
"""
|
|
747
|
+
dialect = self.dialect_name
|
|
748
|
+
if dialect and dialect in MERGE_UNSUPPORTED_DIALECTS:
|
|
749
|
+
self._raise_dialect_not_supported(dialect)
|
|
750
|
+
|
|
751
|
+
def _raise_dialect_not_supported(self, dialect: str) -> None:
|
|
752
|
+
"""Raise error with helpful alternatives for unsupported dialects.
|
|
753
|
+
|
|
754
|
+
Args:
|
|
755
|
+
dialect: Name of the unsupported dialect.
|
|
756
|
+
|
|
757
|
+
Raises:
|
|
758
|
+
DialectNotSupportedError: Always raised with dialect-specific message.
|
|
759
|
+
"""
|
|
760
|
+
alternatives = {
|
|
761
|
+
"mysql": "INSERT ... ON DUPLICATE KEY UPDATE",
|
|
762
|
+
"sqlite": "INSERT ... ON CONFLICT DO UPDATE",
|
|
763
|
+
"duckdb": "INSERT ... ON CONFLICT DO UPDATE",
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
alternative = alternatives.get(dialect, "INSERT ... ON CONFLICT or equivalent")
|
|
767
|
+
msg = (
|
|
768
|
+
f"MERGE statements are not supported in {dialect.upper()}. "
|
|
769
|
+
f"Use {alternative} instead. "
|
|
770
|
+
f"See the SQLSpec documentation for examples."
|
|
771
|
+
)
|
|
772
|
+
raise DialectNotSupportedError(msg)
|
|
773
|
+
|
|
774
|
+
def build(self, dialect: "DialectType" = None) -> "Any":
|
|
775
|
+
"""Build MERGE statement with dialect validation.
|
|
776
|
+
|
|
777
|
+
Args:
|
|
778
|
+
dialect: Optional dialect override for SQL generation.
|
|
779
|
+
|
|
780
|
+
Returns:
|
|
781
|
+
Built statement object.
|
|
782
|
+
"""
|
|
783
|
+
self._validate_dialect_support()
|
|
784
|
+
target_dialect = dialect or self.dialect
|
|
785
|
+
if isinstance(target_dialect, str):
|
|
786
|
+
dialect_name = target_dialect
|
|
787
|
+
elif isinstance(target_dialect, type):
|
|
788
|
+
dialect_name = target_dialect.__name__
|
|
789
|
+
else:
|
|
790
|
+
dialect_name = None
|
|
791
|
+
if dialect_name:
|
|
792
|
+
dialect_name = dialect_name.lower()
|
|
793
|
+
self._normalize_merge_conditions_for_dialect(dialect_name)
|
|
794
|
+
return super().build(dialect=dialect)
|
|
795
|
+
|
|
796
|
+
def _normalize_merge_conditions_for_dialect(self, dialect_name: str | None) -> None:
|
|
797
|
+
"""Normalize WHEN clause conditions for dialect quirks (e.g., Oracle).
|
|
798
|
+
|
|
799
|
+
Oracle requires conditional logic on UPDATE/DELETE branches to live in the
|
|
800
|
+
clause-specific WHERE, not on the WHEN predicate. Move the condition down
|
|
801
|
+
when present so generated SQL matches Oracle syntax.
|
|
802
|
+
"""
|
|
803
|
+
if dialect_name != "oracle":
|
|
804
|
+
return
|
|
805
|
+
|
|
806
|
+
merge_expr = self.get_expression()
|
|
807
|
+
if not isinstance(merge_expr, exp.Merge):
|
|
808
|
+
return
|
|
809
|
+
|
|
810
|
+
whens = merge_expr.args.get("whens")
|
|
811
|
+
if not isinstance(whens, exp.Whens):
|
|
812
|
+
return
|
|
813
|
+
|
|
814
|
+
for when_expr in list(whens.expressions):
|
|
815
|
+
condition_expr = when_expr.args.get("condition")
|
|
816
|
+
if condition_expr is None:
|
|
817
|
+
continue
|
|
818
|
+
then_expr = when_expr.args.get("then")
|
|
819
|
+
if isinstance(then_expr, exp.Update):
|
|
820
|
+
then_expr.set("where", condition_expr)
|
|
821
|
+
when_expr.set("condition", None)
|