sqlspec 0.47.0__cp314-cp314-win_amd64.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.
Files changed (621) hide show
  1. f68e0789eb443ecb1c2c__mypyc.cp314-win_amd64.pyd +0 -0
  2. sqlspec/__init__.py +167 -0
  3. sqlspec/__main__.py +12 -0
  4. sqlspec/__metadata__.py +14 -0
  5. sqlspec/_typing.py +714 -0
  6. sqlspec/adapters/__init__.py +0 -0
  7. sqlspec/adapters/adbc/__init__.py +13 -0
  8. sqlspec/adapters/adbc/_typing.py +106 -0
  9. sqlspec/adapters/adbc/adk/__init__.py +5 -0
  10. sqlspec/adapters/adbc/adk/store.py +1280 -0
  11. sqlspec/adapters/adbc/config.py +378 -0
  12. sqlspec/adapters/adbc/core.cp314-win_amd64.pyd +0 -0
  13. sqlspec/adapters/adbc/core.py +922 -0
  14. sqlspec/adapters/adbc/data_dictionary.py +339 -0
  15. sqlspec/adapters/adbc/driver.py +534 -0
  16. sqlspec/adapters/adbc/events/__init__.py +5 -0
  17. sqlspec/adapters/adbc/events/store.py +285 -0
  18. sqlspec/adapters/adbc/litestar/__init__.py +5 -0
  19. sqlspec/adapters/adbc/litestar/store.py +534 -0
  20. sqlspec/adapters/adbc/type_converter.cp314-win_amd64.pyd +0 -0
  21. sqlspec/adapters/adbc/type_converter.py +142 -0
  22. sqlspec/adapters/aiomysql/__init__.py +21 -0
  23. sqlspec/adapters/aiomysql/_typing.py +137 -0
  24. sqlspec/adapters/aiomysql/adk/__init__.py +5 -0
  25. sqlspec/adapters/aiomysql/adk/store.py +678 -0
  26. sqlspec/adapters/aiomysql/config.py +305 -0
  27. sqlspec/adapters/aiomysql/core.cp314-win_amd64.pyd +0 -0
  28. sqlspec/adapters/aiomysql/core.py +536 -0
  29. sqlspec/adapters/aiomysql/data_dictionary.py +121 -0
  30. sqlspec/adapters/aiomysql/driver.py +386 -0
  31. sqlspec/adapters/aiomysql/events/__init__.py +5 -0
  32. sqlspec/adapters/aiomysql/events/store.py +104 -0
  33. sqlspec/adapters/aiomysql/litestar/__init__.py +5 -0
  34. sqlspec/adapters/aiomysql/litestar/store.py +314 -0
  35. sqlspec/adapters/aiosqlite/__init__.py +26 -0
  36. sqlspec/adapters/aiosqlite/_typing.py +109 -0
  37. sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
  38. sqlspec/adapters/aiosqlite/adk/store.py +829 -0
  39. sqlspec/adapters/aiosqlite/config.py +315 -0
  40. sqlspec/adapters/aiosqlite/core.cp314-win_amd64.pyd +0 -0
  41. sqlspec/adapters/aiosqlite/core.py +315 -0
  42. sqlspec/adapters/aiosqlite/data_dictionary.py +202 -0
  43. sqlspec/adapters/aiosqlite/driver.py +311 -0
  44. sqlspec/adapters/aiosqlite/events/__init__.py +5 -0
  45. sqlspec/adapters/aiosqlite/events/store.py +20 -0
  46. sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
  47. sqlspec/adapters/aiosqlite/litestar/store.py +279 -0
  48. sqlspec/adapters/aiosqlite/pool.cp314-win_amd64.pyd +0 -0
  49. sqlspec/adapters/aiosqlite/pool.py +734 -0
  50. sqlspec/adapters/asyncmy/__init__.py +21 -0
  51. sqlspec/adapters/asyncmy/_typing.py +113 -0
  52. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  53. sqlspec/adapters/asyncmy/adk/store.py +644 -0
  54. sqlspec/adapters/asyncmy/config.py +307 -0
  55. sqlspec/adapters/asyncmy/core.cp314-win_amd64.pyd +0 -0
  56. sqlspec/adapters/asyncmy/core.py +538 -0
  57. sqlspec/adapters/asyncmy/data_dictionary.py +122 -0
  58. sqlspec/adapters/asyncmy/driver.py +391 -0
  59. sqlspec/adapters/asyncmy/events/__init__.py +5 -0
  60. sqlspec/adapters/asyncmy/events/store.py +104 -0
  61. sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
  62. sqlspec/adapters/asyncmy/litestar/store.py +296 -0
  63. sqlspec/adapters/asyncpg/__init__.py +26 -0
  64. sqlspec/adapters/asyncpg/_typing.py +103 -0
  65. sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
  66. sqlspec/adapters/asyncpg/adk/store.py +483 -0
  67. sqlspec/adapters/asyncpg/config.py +575 -0
  68. sqlspec/adapters/asyncpg/core.cp314-win_amd64.pyd +0 -0
  69. sqlspec/adapters/asyncpg/core.py +480 -0
  70. sqlspec/adapters/asyncpg/data_dictionary.py +157 -0
  71. sqlspec/adapters/asyncpg/driver.py +487 -0
  72. sqlspec/adapters/asyncpg/events/__init__.py +6 -0
  73. sqlspec/adapters/asyncpg/events/_hub.py +181 -0
  74. sqlspec/adapters/asyncpg/events/backend.py +210 -0
  75. sqlspec/adapters/asyncpg/events/store.py +40 -0
  76. sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
  77. sqlspec/adapters/asyncpg/litestar/store.py +251 -0
  78. sqlspec/adapters/bigquery/__init__.py +15 -0
  79. sqlspec/adapters/bigquery/_typing.py +108 -0
  80. sqlspec/adapters/bigquery/config.py +362 -0
  81. sqlspec/adapters/bigquery/core.cp314-win_amd64.pyd +0 -0
  82. sqlspec/adapters/bigquery/core.py +768 -0
  83. sqlspec/adapters/bigquery/data_dictionary.py +120 -0
  84. sqlspec/adapters/bigquery/driver.py +542 -0
  85. sqlspec/adapters/bigquery/events/__init__.py +5 -0
  86. sqlspec/adapters/bigquery/events/store.py +139 -0
  87. sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
  88. sqlspec/adapters/bigquery/litestar/store.py +325 -0
  89. sqlspec/adapters/bigquery/type_converter.cp314-win_amd64.pyd +0 -0
  90. sqlspec/adapters/bigquery/type_converter.py +107 -0
  91. sqlspec/adapters/cockroach_asyncpg/__init__.py +26 -0
  92. sqlspec/adapters/cockroach_asyncpg/_typing.py +73 -0
  93. sqlspec/adapters/cockroach_asyncpg/adk/__init__.py +3 -0
  94. sqlspec/adapters/cockroach_asyncpg/adk/store.py +465 -0
  95. sqlspec/adapters/cockroach_asyncpg/config.py +248 -0
  96. sqlspec/adapters/cockroach_asyncpg/core.cp314-win_amd64.pyd +0 -0
  97. sqlspec/adapters/cockroach_asyncpg/core.py +55 -0
  98. sqlspec/adapters/cockroach_asyncpg/data_dictionary.py +110 -0
  99. sqlspec/adapters/cockroach_asyncpg/driver.py +142 -0
  100. sqlspec/adapters/cockroach_asyncpg/events/__init__.py +3 -0
  101. sqlspec/adapters/cockroach_asyncpg/events/store.py +20 -0
  102. sqlspec/adapters/cockroach_asyncpg/litestar/__init__.py +3 -0
  103. sqlspec/adapters/cockroach_asyncpg/litestar/store.py +142 -0
  104. sqlspec/adapters/cockroach_psycopg/__init__.py +39 -0
  105. sqlspec/adapters/cockroach_psycopg/_typing.py +137 -0
  106. sqlspec/adapters/cockroach_psycopg/adk/__init__.py +13 -0
  107. sqlspec/adapters/cockroach_psycopg/adk/store.py +1039 -0
  108. sqlspec/adapters/cockroach_psycopg/config.py +511 -0
  109. sqlspec/adapters/cockroach_psycopg/core.cp314-win_amd64.pyd +0 -0
  110. sqlspec/adapters/cockroach_psycopg/core.py +63 -0
  111. sqlspec/adapters/cockroach_psycopg/data_dictionary.py +220 -0
  112. sqlspec/adapters/cockroach_psycopg/driver.py +273 -0
  113. sqlspec/adapters/cockroach_psycopg/events/__init__.py +6 -0
  114. sqlspec/adapters/cockroach_psycopg/events/store.py +34 -0
  115. sqlspec/adapters/cockroach_psycopg/litestar/__init__.py +3 -0
  116. sqlspec/adapters/cockroach_psycopg/litestar/store.py +327 -0
  117. sqlspec/adapters/duckdb/__init__.py +29 -0
  118. sqlspec/adapters/duckdb/_typing.py +104 -0
  119. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  120. sqlspec/adapters/duckdb/adk/store.py +935 -0
  121. sqlspec/adapters/duckdb/config.py +386 -0
  122. sqlspec/adapters/duckdb/core.cp314-win_amd64.pyd +0 -0
  123. sqlspec/adapters/duckdb/core.py +332 -0
  124. sqlspec/adapters/duckdb/data_dictionary.py +140 -0
  125. sqlspec/adapters/duckdb/driver.py +426 -0
  126. sqlspec/adapters/duckdb/events/__init__.py +5 -0
  127. sqlspec/adapters/duckdb/events/store.py +57 -0
  128. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  129. sqlspec/adapters/duckdb/litestar/store.py +330 -0
  130. sqlspec/adapters/duckdb/pool.cp314-win_amd64.pyd +0 -0
  131. sqlspec/adapters/duckdb/pool.py +350 -0
  132. sqlspec/adapters/duckdb/type_converter.cp314-win_amd64.pyd +0 -0
  133. sqlspec/adapters/duckdb/type_converter.py +118 -0
  134. sqlspec/adapters/mysqlconnector/__init__.py +39 -0
  135. sqlspec/adapters/mysqlconnector/_typing.py +186 -0
  136. sqlspec/adapters/mysqlconnector/adk/__init__.py +15 -0
  137. sqlspec/adapters/mysqlconnector/adk/store.py +1183 -0
  138. sqlspec/adapters/mysqlconnector/config.py +421 -0
  139. sqlspec/adapters/mysqlconnector/core.cp314-win_amd64.pyd +0 -0
  140. sqlspec/adapters/mysqlconnector/core.py +472 -0
  141. sqlspec/adapters/mysqlconnector/data_dictionary.py +230 -0
  142. sqlspec/adapters/mysqlconnector/driver.py +516 -0
  143. sqlspec/adapters/mysqlconnector/events/__init__.py +8 -0
  144. sqlspec/adapters/mysqlconnector/events/store.py +98 -0
  145. sqlspec/adapters/mysqlconnector/litestar/__init__.py +5 -0
  146. sqlspec/adapters/mysqlconnector/litestar/store.py +426 -0
  147. sqlspec/adapters/oracledb/__init__.py +39 -0
  148. sqlspec/adapters/oracledb/_json_handlers.cp314-win_amd64.pyd +0 -0
  149. sqlspec/adapters/oracledb/_json_handlers.py +196 -0
  150. sqlspec/adapters/oracledb/_param_types.cp314-win_amd64.pyd +0 -0
  151. sqlspec/adapters/oracledb/_param_types.py +46 -0
  152. sqlspec/adapters/oracledb/_typing.py +258 -0
  153. sqlspec/adapters/oracledb/_uuid_handlers.cp314-win_amd64.pyd +0 -0
  154. sqlspec/adapters/oracledb/_uuid_handlers.py +163 -0
  155. sqlspec/adapters/oracledb/_vector_handlers.cp314-win_amd64.pyd +0 -0
  156. sqlspec/adapters/oracledb/_vector_handlers.py +228 -0
  157. sqlspec/adapters/oracledb/adk/__init__.py +21 -0
  158. sqlspec/adapters/oracledb/adk/store.py +2453 -0
  159. sqlspec/adapters/oracledb/config.py +575 -0
  160. sqlspec/adapters/oracledb/core.cp314-win_amd64.pyd +0 -0
  161. sqlspec/adapters/oracledb/core.py +820 -0
  162. sqlspec/adapters/oracledb/data_dictionary.py +404 -0
  163. sqlspec/adapters/oracledb/driver.py +1277 -0
  164. sqlspec/adapters/oracledb/events/__init__.py +16 -0
  165. sqlspec/adapters/oracledb/events/_hub.py +345 -0
  166. sqlspec/adapters/oracledb/events/backend.py +300 -0
  167. sqlspec/adapters/oracledb/events/store.py +420 -0
  168. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  169. sqlspec/adapters/oracledb/litestar/store.py +781 -0
  170. sqlspec/adapters/oracledb/migrations.py +539 -0
  171. sqlspec/adapters/oracledb/type_converter.cp314-win_amd64.pyd +0 -0
  172. sqlspec/adapters/oracledb/type_converter.py +211 -0
  173. sqlspec/adapters/psqlpy/__init__.py +18 -0
  174. sqlspec/adapters/psqlpy/_typing.py +121 -0
  175. sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
  176. sqlspec/adapters/psqlpy/adk/store.py +591 -0
  177. sqlspec/adapters/psqlpy/config.py +376 -0
  178. sqlspec/adapters/psqlpy/core.cp314-win_amd64.pyd +0 -0
  179. sqlspec/adapters/psqlpy/core.py +694 -0
  180. sqlspec/adapters/psqlpy/data_dictionary.py +121 -0
  181. sqlspec/adapters/psqlpy/driver.py +411 -0
  182. sqlspec/adapters/psqlpy/events/__init__.py +6 -0
  183. sqlspec/adapters/psqlpy/events/_hub.py +204 -0
  184. sqlspec/adapters/psqlpy/events/backend.py +210 -0
  185. sqlspec/adapters/psqlpy/events/store.py +20 -0
  186. sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
  187. sqlspec/adapters/psqlpy/litestar/store.py +270 -0
  188. sqlspec/adapters/psqlpy/type_converter.cp314-win_amd64.pyd +0 -0
  189. sqlspec/adapters/psqlpy/type_converter.py +113 -0
  190. sqlspec/adapters/psycopg/__init__.py +38 -0
  191. sqlspec/adapters/psycopg/_typing.py +218 -0
  192. sqlspec/adapters/psycopg/adk/__init__.py +10 -0
  193. sqlspec/adapters/psycopg/adk/store.py +1106 -0
  194. sqlspec/adapters/psycopg/config.py +695 -0
  195. sqlspec/adapters/psycopg/core.cp314-win_amd64.pyd +0 -0
  196. sqlspec/adapters/psycopg/core.py +520 -0
  197. sqlspec/adapters/psycopg/data_dictionary.py +278 -0
  198. sqlspec/adapters/psycopg/driver.py +1033 -0
  199. sqlspec/adapters/psycopg/events/__init__.py +20 -0
  200. sqlspec/adapters/psycopg/events/_hub.py +388 -0
  201. sqlspec/adapters/psycopg/events/backend.py +398 -0
  202. sqlspec/adapters/psycopg/events/store.py +42 -0
  203. sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
  204. sqlspec/adapters/psycopg/litestar/store.py +554 -0
  205. sqlspec/adapters/psycopg/type_converter.cp314-win_amd64.pyd +0 -0
  206. sqlspec/adapters/psycopg/type_converter.py +93 -0
  207. sqlspec/adapters/pymysql/__init__.py +21 -0
  208. sqlspec/adapters/pymysql/_typing.py +92 -0
  209. sqlspec/adapters/pymysql/adk/__init__.py +5 -0
  210. sqlspec/adapters/pymysql/adk/store.py +657 -0
  211. sqlspec/adapters/pymysql/config.py +176 -0
  212. sqlspec/adapters/pymysql/core.cp314-win_amd64.pyd +0 -0
  213. sqlspec/adapters/pymysql/core.py +469 -0
  214. sqlspec/adapters/pymysql/data_dictionary.py +120 -0
  215. sqlspec/adapters/pymysql/driver.py +271 -0
  216. sqlspec/adapters/pymysql/events/__init__.py +5 -0
  217. sqlspec/adapters/pymysql/events/store.py +50 -0
  218. sqlspec/adapters/pymysql/litestar/__init__.py +5 -0
  219. sqlspec/adapters/pymysql/litestar/store.py +232 -0
  220. sqlspec/adapters/pymysql/pool.cp314-win_amd64.pyd +0 -0
  221. sqlspec/adapters/pymysql/pool.py +184 -0
  222. sqlspec/adapters/spanner/__init__.py +33 -0
  223. sqlspec/adapters/spanner/_typing.py +102 -0
  224. sqlspec/adapters/spanner/adk/__init__.py +5 -0
  225. sqlspec/adapters/spanner/adk/store.py +758 -0
  226. sqlspec/adapters/spanner/config.py +355 -0
  227. sqlspec/adapters/spanner/core.cp314-win_amd64.pyd +0 -0
  228. sqlspec/adapters/spanner/core.py +263 -0
  229. sqlspec/adapters/spanner/data_dictionary.py +120 -0
  230. sqlspec/adapters/spanner/driver.py +407 -0
  231. sqlspec/adapters/spanner/events/__init__.py +5 -0
  232. sqlspec/adapters/spanner/events/store.py +187 -0
  233. sqlspec/adapters/spanner/litestar/__init__.py +5 -0
  234. sqlspec/adapters/spanner/litestar/store.py +291 -0
  235. sqlspec/adapters/spanner/type_converter.cp314-win_amd64.pyd +0 -0
  236. sqlspec/adapters/spanner/type_converter.py +342 -0
  237. sqlspec/adapters/sqlite/__init__.py +19 -0
  238. sqlspec/adapters/sqlite/_typing.py +123 -0
  239. sqlspec/adapters/sqlite/adk/__init__.py +5 -0
  240. sqlspec/adapters/sqlite/adk/store.py +992 -0
  241. sqlspec/adapters/sqlite/config.py +240 -0
  242. sqlspec/adapters/sqlite/core.cp314-win_amd64.pyd +0 -0
  243. sqlspec/adapters/sqlite/core.py +357 -0
  244. sqlspec/adapters/sqlite/data_dictionary.py +198 -0
  245. sqlspec/adapters/sqlite/driver.py +527 -0
  246. sqlspec/adapters/sqlite/events/__init__.py +5 -0
  247. sqlspec/adapters/sqlite/events/store.py +20 -0
  248. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  249. sqlspec/adapters/sqlite/litestar/store.py +316 -0
  250. sqlspec/adapters/sqlite/pool.cp314-win_amd64.pyd +0 -0
  251. sqlspec/adapters/sqlite/pool.py +237 -0
  252. sqlspec/adapters/sqlite/type_converter.cp314-win_amd64.pyd +0 -0
  253. sqlspec/adapters/sqlite/type_converter.py +114 -0
  254. sqlspec/base.py +832 -0
  255. sqlspec/builder/__init__.py +181 -0
  256. sqlspec/builder/_base.cp314-win_amd64.pyd +0 -0
  257. sqlspec/builder/_base.py +1071 -0
  258. sqlspec/builder/_column.cp314-win_amd64.pyd +0 -0
  259. sqlspec/builder/_column.py +521 -0
  260. sqlspec/builder/_ddl.cp314-win_amd64.pyd +0 -0
  261. sqlspec/builder/_ddl.py +1691 -0
  262. sqlspec/builder/_delete.cp314-win_amd64.pyd +0 -0
  263. sqlspec/builder/_delete.py +95 -0
  264. sqlspec/builder/_dml.cp314-win_amd64.pyd +0 -0
  265. sqlspec/builder/_dml.py +386 -0
  266. sqlspec/builder/_explain.cp314-win_amd64.pyd +0 -0
  267. sqlspec/builder/_explain.py +579 -0
  268. sqlspec/builder/_expression_wrappers.cp314-win_amd64.pyd +0 -0
  269. sqlspec/builder/_expression_wrappers.py +46 -0
  270. sqlspec/builder/_factory.cp314-win_amd64.pyd +0 -0
  271. sqlspec/builder/_factory.py +1884 -0
  272. sqlspec/builder/_insert.cp314-win_amd64.pyd +0 -0
  273. sqlspec/builder/_insert.py +405 -0
  274. sqlspec/builder/_join.cp314-win_amd64.pyd +0 -0
  275. sqlspec/builder/_join.py +489 -0
  276. sqlspec/builder/_merge.cp314-win_amd64.pyd +0 -0
  277. sqlspec/builder/_merge.py +823 -0
  278. sqlspec/builder/_parsing_utils.cp314-win_amd64.pyd +0 -0
  279. sqlspec/builder/_parsing_utils.py +295 -0
  280. sqlspec/builder/_select.cp314-win_amd64.pyd +0 -0
  281. sqlspec/builder/_select.py +1666 -0
  282. sqlspec/builder/_temporal.cp314-win_amd64.pyd +0 -0
  283. sqlspec/builder/_temporal.py +167 -0
  284. sqlspec/builder/_update.cp314-win_amd64.pyd +0 -0
  285. sqlspec/builder/_update.py +173 -0
  286. sqlspec/builder/_vector_distance.cp314-win_amd64.pyd +0 -0
  287. sqlspec/builder/_vector_distance.py +330 -0
  288. sqlspec/cli.py +1095 -0
  289. sqlspec/config.py +2383 -0
  290. sqlspec/core/__init__.py +372 -0
  291. sqlspec/core/_correlation.cp314-win_amd64.pyd +0 -0
  292. sqlspec/core/_correlation.py +176 -0
  293. sqlspec/core/_pagination.py +42 -0
  294. sqlspec/core/_pool.cp314-win_amd64.pyd +0 -0
  295. sqlspec/core/_pool.py +76 -0
  296. sqlspec/core/cache.cp314-win_amd64.pyd +0 -0
  297. sqlspec/core/cache.py +1085 -0
  298. sqlspec/core/compiler.cp314-win_amd64.pyd +0 -0
  299. sqlspec/core/compiler.py +1090 -0
  300. sqlspec/core/config_runtime.cp314-win_amd64.pyd +0 -0
  301. sqlspec/core/config_runtime.py +174 -0
  302. sqlspec/core/explain.cp314-win_amd64.pyd +0 -0
  303. sqlspec/core/explain.py +275 -0
  304. sqlspec/core/filters.cp314-win_amd64.pyd +0 -0
  305. sqlspec/core/filters.py +969 -0
  306. sqlspec/core/hashing.cp314-win_amd64.pyd +0 -0
  307. sqlspec/core/hashing.py +266 -0
  308. sqlspec/core/metrics.cp314-win_amd64.pyd +0 -0
  309. sqlspec/core/metrics.py +83 -0
  310. sqlspec/core/parameters/__init__.py +72 -0
  311. sqlspec/core/parameters/_alignment.cp314-win_amd64.pyd +0 -0
  312. sqlspec/core/parameters/_alignment.py +283 -0
  313. sqlspec/core/parameters/_converter.cp314-win_amd64.pyd +0 -0
  314. sqlspec/core/parameters/_converter.py +554 -0
  315. sqlspec/core/parameters/_processor.cp314-win_amd64.pyd +0 -0
  316. sqlspec/core/parameters/_processor.py +1182 -0
  317. sqlspec/core/parameters/_registry.cp314-win_amd64.pyd +0 -0
  318. sqlspec/core/parameters/_registry.py +206 -0
  319. sqlspec/core/parameters/_transformers.cp314-win_amd64.pyd +0 -0
  320. sqlspec/core/parameters/_transformers.py +324 -0
  321. sqlspec/core/parameters/_types.cp314-win_amd64.pyd +0 -0
  322. sqlspec/core/parameters/_types.py +536 -0
  323. sqlspec/core/parameters/_validator.cp314-win_amd64.pyd +0 -0
  324. sqlspec/core/parameters/_validator.py +171 -0
  325. sqlspec/core/pipeline.cp314-win_amd64.pyd +0 -0
  326. sqlspec/core/pipeline.py +333 -0
  327. sqlspec/core/query_modifiers.cp314-win_amd64.pyd +0 -0
  328. sqlspec/core/query_modifiers.py +508 -0
  329. sqlspec/core/result/__init__.py +25 -0
  330. sqlspec/core/result/_base.cp314-win_amd64.pyd +0 -0
  331. sqlspec/core/result/_base.py +1232 -0
  332. sqlspec/core/result/_io.cp314-win_amd64.pyd +0 -0
  333. sqlspec/core/result/_io.py +28 -0
  334. sqlspec/core/splitter.cp314-win_amd64.pyd +0 -0
  335. sqlspec/core/splitter.py +1021 -0
  336. sqlspec/core/sqlcommenter.cp314-win_amd64.pyd +0 -0
  337. sqlspec/core/sqlcommenter.py +249 -0
  338. sqlspec/core/stack.cp314-win_amd64.pyd +0 -0
  339. sqlspec/core/stack.py +163 -0
  340. sqlspec/core/statement.cp314-win_amd64.pyd +0 -0
  341. sqlspec/core/statement.py +1865 -0
  342. sqlspec/core/type_converter.cp314-win_amd64.pyd +0 -0
  343. sqlspec/core/type_converter.py +340 -0
  344. sqlspec/data_dictionary/__init__.py +22 -0
  345. sqlspec/data_dictionary/_loader.cp314-win_amd64.pyd +0 -0
  346. sqlspec/data_dictionary/_loader.py +138 -0
  347. sqlspec/data_dictionary/_registry.cp314-win_amd64.pyd +0 -0
  348. sqlspec/data_dictionary/_registry.py +74 -0
  349. sqlspec/data_dictionary/_types.cp314-win_amd64.pyd +0 -0
  350. sqlspec/data_dictionary/_types.py +121 -0
  351. sqlspec/data_dictionary/dialects/__init__.py +21 -0
  352. sqlspec/data_dictionary/dialects/bigquery.cp314-win_amd64.pyd +0 -0
  353. sqlspec/data_dictionary/dialects/bigquery.py +81 -0
  354. sqlspec/data_dictionary/dialects/cockroachdb.cp314-win_amd64.pyd +0 -0
  355. sqlspec/data_dictionary/dialects/cockroachdb.py +54 -0
  356. sqlspec/data_dictionary/dialects/duckdb.cp314-win_amd64.pyd +0 -0
  357. sqlspec/data_dictionary/dialects/duckdb.py +47 -0
  358. sqlspec/data_dictionary/dialects/mysql.cp314-win_amd64.pyd +0 -0
  359. sqlspec/data_dictionary/dialects/mysql.py +53 -0
  360. sqlspec/data_dictionary/dialects/oracle.cp314-win_amd64.pyd +0 -0
  361. sqlspec/data_dictionary/dialects/oracle.py +197 -0
  362. sqlspec/data_dictionary/dialects/postgres.cp314-win_amd64.pyd +0 -0
  363. sqlspec/data_dictionary/dialects/postgres.py +69 -0
  364. sqlspec/data_dictionary/dialects/spanner.cp314-win_amd64.pyd +0 -0
  365. sqlspec/data_dictionary/dialects/spanner.py +37 -0
  366. sqlspec/data_dictionary/dialects/sqlite.cp314-win_amd64.pyd +0 -0
  367. sqlspec/data_dictionary/dialects/sqlite.py +59 -0
  368. sqlspec/data_dictionary/sql/.gitkeep +0 -0
  369. sqlspec/data_dictionary/sql/bigquery/columns.sql +23 -0
  370. sqlspec/data_dictionary/sql/bigquery/foreign_keys.sql +34 -0
  371. sqlspec/data_dictionary/sql/bigquery/indexes.sql +19 -0
  372. sqlspec/data_dictionary/sql/bigquery/tables.sql +33 -0
  373. sqlspec/data_dictionary/sql/bigquery/version.sql +3 -0
  374. sqlspec/data_dictionary/sql/cockroachdb/columns.sql +34 -0
  375. sqlspec/data_dictionary/sql/cockroachdb/foreign_keys.sql +40 -0
  376. sqlspec/data_dictionary/sql/cockroachdb/indexes.sql +32 -0
  377. sqlspec/data_dictionary/sql/cockroachdb/tables.sql +44 -0
  378. sqlspec/data_dictionary/sql/cockroachdb/version.sql +3 -0
  379. sqlspec/data_dictionary/sql/duckdb/columns.sql +23 -0
  380. sqlspec/data_dictionary/sql/duckdb/foreign_keys.sql +36 -0
  381. sqlspec/data_dictionary/sql/duckdb/indexes.sql +19 -0
  382. sqlspec/data_dictionary/sql/duckdb/tables.sql +38 -0
  383. sqlspec/data_dictionary/sql/duckdb/version.sql +3 -0
  384. sqlspec/data_dictionary/sql/mysql/columns.sql +23 -0
  385. sqlspec/data_dictionary/sql/mysql/foreign_keys.sql +28 -0
  386. sqlspec/data_dictionary/sql/mysql/indexes.sql +26 -0
  387. sqlspec/data_dictionary/sql/mysql/tables.sql +33 -0
  388. sqlspec/data_dictionary/sql/mysql/version.sql +3 -0
  389. sqlspec/data_dictionary/sql/oracle/columns.sql +23 -0
  390. sqlspec/data_dictionary/sql/oracle/foreign_keys.sql +48 -0
  391. sqlspec/data_dictionary/sql/oracle/indexes.sql +44 -0
  392. sqlspec/data_dictionary/sql/oracle/tables.sql +25 -0
  393. sqlspec/data_dictionary/sql/oracle/version.sql +20 -0
  394. sqlspec/data_dictionary/sql/postgres/columns.sql +34 -0
  395. sqlspec/data_dictionary/sql/postgres/foreign_keys.sql +40 -0
  396. sqlspec/data_dictionary/sql/postgres/indexes.sql +56 -0
  397. sqlspec/data_dictionary/sql/postgres/tables.sql +44 -0
  398. sqlspec/data_dictionary/sql/postgres/version.sql +3 -0
  399. sqlspec/data_dictionary/sql/spanner/columns.sql +23 -0
  400. sqlspec/data_dictionary/sql/spanner/foreign_keys.sql +70 -0
  401. sqlspec/data_dictionary/sql/spanner/indexes.sql +30 -0
  402. sqlspec/data_dictionary/sql/spanner/tables.sql +9 -0
  403. sqlspec/data_dictionary/sql/spanner/version.sql +3 -0
  404. sqlspec/data_dictionary/sql/sqlite/columns.sql +23 -0
  405. sqlspec/data_dictionary/sql/sqlite/foreign_keys.sql +22 -0
  406. sqlspec/data_dictionary/sql/sqlite/indexes.sql +7 -0
  407. sqlspec/data_dictionary/sql/sqlite/tables.sql +28 -0
  408. sqlspec/data_dictionary/sql/sqlite/version.sql +3 -0
  409. sqlspec/dialects/__init__.py +22 -0
  410. sqlspec/dialects/_compat.cp314-win_amd64.pyd +0 -0
  411. sqlspec/dialects/_compat.py +14 -0
  412. sqlspec/dialects/postgres/__init__.py +9 -0
  413. sqlspec/dialects/postgres/_generators.cp314-win_amd64.pyd +0 -0
  414. sqlspec/dialects/postgres/_generators.py +57 -0
  415. sqlspec/dialects/postgres/_operators.cp314-win_amd64.pyd +0 -0
  416. sqlspec/dialects/postgres/_operators.py +81 -0
  417. sqlspec/dialects/postgres/_paradedb.py +50 -0
  418. sqlspec/dialects/postgres/_pgvector.py +36 -0
  419. sqlspec/dialects/spanner/__init__.py +6 -0
  420. sqlspec/dialects/spanner/_generators.cp314-win_amd64.pyd +0 -0
  421. sqlspec/dialects/spanner/_generators.py +206 -0
  422. sqlspec/dialects/spanner/_spangres.py +77 -0
  423. sqlspec/dialects/spanner/_spanner.py +179 -0
  424. sqlspec/driver/__init__.py +49 -0
  425. sqlspec/driver/_async.cp314-win_amd64.pyd +0 -0
  426. sqlspec/driver/_async.py +1830 -0
  427. sqlspec/driver/_common.cp314-win_amd64.pyd +0 -0
  428. sqlspec/driver/_common.py +2292 -0
  429. sqlspec/driver/_exception_handler.cp314-win_amd64.pyd +0 -0
  430. sqlspec/driver/_exception_handler.py +108 -0
  431. sqlspec/driver/_query_cache.cp314-win_amd64.pyd +0 -0
  432. sqlspec/driver/_query_cache.py +96 -0
  433. sqlspec/driver/_sql_helpers.cp314-win_amd64.pyd +0 -0
  434. sqlspec/driver/_sql_helpers.py +139 -0
  435. sqlspec/driver/_storage_helpers.cp314-win_amd64.pyd +0 -0
  436. sqlspec/driver/_storage_helpers.py +153 -0
  437. sqlspec/driver/_sync.cp314-win_amd64.pyd +0 -0
  438. sqlspec/driver/_sync.py +1817 -0
  439. sqlspec/exceptions.cp314-win_amd64.pyd +0 -0
  440. sqlspec/exceptions.py +480 -0
  441. sqlspec/extensions/__init__.py +0 -0
  442. sqlspec/extensions/adk/__init__.py +84 -0
  443. sqlspec/extensions/adk/_config_utils.py +199 -0
  444. sqlspec/extensions/adk/_types.cp314-win_amd64.pyd +0 -0
  445. sqlspec/extensions/adk/_types.py +41 -0
  446. sqlspec/extensions/adk/artifact/__init__.py +57 -0
  447. sqlspec/extensions/adk/artifact/_types.cp314-win_amd64.pyd +0 -0
  448. sqlspec/extensions/adk/artifact/_types.py +32 -0
  449. sqlspec/extensions/adk/artifact/service.py +508 -0
  450. sqlspec/extensions/adk/artifact/store.py +361 -0
  451. sqlspec/extensions/adk/converters.py +212 -0
  452. sqlspec/extensions/adk/memory/__init__.py +69 -0
  453. sqlspec/extensions/adk/memory/_types.cp314-win_amd64.pyd +0 -0
  454. sqlspec/extensions/adk/memory/_types.py +30 -0
  455. sqlspec/extensions/adk/memory/converters.py +225 -0
  456. sqlspec/extensions/adk/memory/service.py +316 -0
  457. sqlspec/extensions/adk/memory/store.py +525 -0
  458. sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +184 -0
  459. sqlspec/extensions/adk/migrations/__init__.py +0 -0
  460. sqlspec/extensions/adk/service.py +279 -0
  461. sqlspec/extensions/adk/store.py +590 -0
  462. sqlspec/extensions/events/__init__.py +51 -0
  463. sqlspec/extensions/events/_channel.py +703 -0
  464. sqlspec/extensions/events/_hints.cp314-win_amd64.pyd +0 -0
  465. sqlspec/extensions/events/_hints.py +45 -0
  466. sqlspec/extensions/events/_models.py +23 -0
  467. sqlspec/extensions/events/_payload.cp314-win_amd64.pyd +0 -0
  468. sqlspec/extensions/events/_payload.py +69 -0
  469. sqlspec/extensions/events/_protocols.py +134 -0
  470. sqlspec/extensions/events/_queue.py +462 -0
  471. sqlspec/extensions/events/_store.py +209 -0
  472. sqlspec/extensions/events/migrations/0001_create_event_queue.py +59 -0
  473. sqlspec/extensions/events/migrations/__init__.py +3 -0
  474. sqlspec/extensions/fastapi/__init__.py +22 -0
  475. sqlspec/extensions/fastapi/extension.py +391 -0
  476. sqlspec/extensions/fastapi/providers.cp314-win_amd64.pyd +0 -0
  477. sqlspec/extensions/fastapi/providers.py +712 -0
  478. sqlspec/extensions/flask/__init__.py +38 -0
  479. sqlspec/extensions/flask/_state.py +87 -0
  480. sqlspec/extensions/flask/_utils.py +71 -0
  481. sqlspec/extensions/flask/extension.py +539 -0
  482. sqlspec/extensions/litestar/__init__.py +31 -0
  483. sqlspec/extensions/litestar/_utils.py +52 -0
  484. sqlspec/extensions/litestar/channels.py +165 -0
  485. sqlspec/extensions/litestar/cli.py +102 -0
  486. sqlspec/extensions/litestar/config.py +90 -0
  487. sqlspec/extensions/litestar/handlers.py +316 -0
  488. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  489. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  490. sqlspec/extensions/litestar/plugin.py +1066 -0
  491. sqlspec/extensions/litestar/providers.cp314-win_amd64.pyd +0 -0
  492. sqlspec/extensions/litestar/providers.py +784 -0
  493. sqlspec/extensions/litestar/store.py +298 -0
  494. sqlspec/extensions/otel/__init__.py +58 -0
  495. sqlspec/extensions/prometheus/__init__.py +113 -0
  496. sqlspec/extensions/sanic/__init__.py +19 -0
  497. sqlspec/extensions/sanic/_state.py +43 -0
  498. sqlspec/extensions/sanic/_utils.py +127 -0
  499. sqlspec/extensions/sanic/extension.py +647 -0
  500. sqlspec/extensions/starlette/__init__.py +22 -0
  501. sqlspec/extensions/starlette/_state.py +42 -0
  502. sqlspec/extensions/starlette/_utils.py +96 -0
  503. sqlspec/extensions/starlette/extension.py +374 -0
  504. sqlspec/extensions/starlette/middleware.py +281 -0
  505. sqlspec/loader.cp314-win_amd64.pyd +0 -0
  506. sqlspec/loader.py +727 -0
  507. sqlspec/migrations/__init__.py +39 -0
  508. sqlspec/migrations/base.cp314-win_amd64.pyd +0 -0
  509. sqlspec/migrations/base.py +862 -0
  510. sqlspec/migrations/commands.py +2151 -0
  511. sqlspec/migrations/context.cp314-win_amd64.pyd +0 -0
  512. sqlspec/migrations/context.py +157 -0
  513. sqlspec/migrations/fix.cp314-win_amd64.pyd +0 -0
  514. sqlspec/migrations/fix.py +204 -0
  515. sqlspec/migrations/loaders.cp314-win_amd64.pyd +0 -0
  516. sqlspec/migrations/loaders.py +443 -0
  517. sqlspec/migrations/runner.cp314-win_amd64.pyd +0 -0
  518. sqlspec/migrations/runner.py +1195 -0
  519. sqlspec/migrations/squash.cp314-win_amd64.pyd +0 -0
  520. sqlspec/migrations/squash.py +490 -0
  521. sqlspec/migrations/templates.cp314-win_amd64.pyd +0 -0
  522. sqlspec/migrations/templates.py +234 -0
  523. sqlspec/migrations/tracker.cp314-win_amd64.pyd +0 -0
  524. sqlspec/migrations/tracker.py +792 -0
  525. sqlspec/migrations/utils.cp314-win_amd64.pyd +0 -0
  526. sqlspec/migrations/utils.py +256 -0
  527. sqlspec/migrations/validation.cp314-win_amd64.pyd +0 -0
  528. sqlspec/migrations/validation.py +359 -0
  529. sqlspec/migrations/version.cp314-win_amd64.pyd +0 -0
  530. sqlspec/migrations/version.py +446 -0
  531. sqlspec/observability/__init__.py +57 -0
  532. sqlspec/observability/_common.cp314-win_amd64.pyd +0 -0
  533. sqlspec/observability/_common.py +77 -0
  534. sqlspec/observability/_config.cp314-win_amd64.pyd +0 -0
  535. sqlspec/observability/_config.py +364 -0
  536. sqlspec/observability/_diagnostics.cp314-win_amd64.pyd +0 -0
  537. sqlspec/observability/_diagnostics.py +74 -0
  538. sqlspec/observability/_dispatcher.cp314-win_amd64.pyd +0 -0
  539. sqlspec/observability/_dispatcher.py +200 -0
  540. sqlspec/observability/_formatters/__init__.py +13 -0
  541. sqlspec/observability/_formatters/_aws.cp314-win_amd64.pyd +0 -0
  542. sqlspec/observability/_formatters/_aws.py +102 -0
  543. sqlspec/observability/_formatters/_azure.cp314-win_amd64.pyd +0 -0
  544. sqlspec/observability/_formatters/_azure.py +96 -0
  545. sqlspec/observability/_formatters/_base.cp314-win_amd64.pyd +0 -0
  546. sqlspec/observability/_formatters/_base.py +57 -0
  547. sqlspec/observability/_formatters/_gcp.cp314-win_amd64.pyd +0 -0
  548. sqlspec/observability/_formatters/_gcp.py +131 -0
  549. sqlspec/observability/_formatting.py +58 -0
  550. sqlspec/observability/_observer.cp314-win_amd64.pyd +0 -0
  551. sqlspec/observability/_observer.py +361 -0
  552. sqlspec/observability/_runtime.cp314-win_amd64.pyd +0 -0
  553. sqlspec/observability/_runtime.py +461 -0
  554. sqlspec/observability/_sampling.cp314-win_amd64.pyd +0 -0
  555. sqlspec/observability/_sampling.py +188 -0
  556. sqlspec/observability/_spans.cp314-win_amd64.pyd +0 -0
  557. sqlspec/observability/_spans.py +161 -0
  558. sqlspec/protocols.py +955 -0
  559. sqlspec/py.typed +0 -0
  560. sqlspec/service.py +433 -0
  561. sqlspec/storage/__init__.py +48 -0
  562. sqlspec/storage/_arrow_payload.py +68 -0
  563. sqlspec/storage/_paths.cp314-win_amd64.pyd +0 -0
  564. sqlspec/storage/_paths.py +58 -0
  565. sqlspec/storage/_utils.py +46 -0
  566. sqlspec/storage/backends/__init__.py +1 -0
  567. sqlspec/storage/backends/base.cp314-win_amd64.pyd +0 -0
  568. sqlspec/storage/backends/base.py +374 -0
  569. sqlspec/storage/backends/fsspec.py +574 -0
  570. sqlspec/storage/backends/local.py +468 -0
  571. sqlspec/storage/backends/obstore.py +956 -0
  572. sqlspec/storage/errors.cp314-win_amd64.pyd +0 -0
  573. sqlspec/storage/errors.py +102 -0
  574. sqlspec/storage/pipeline.cp314-win_amd64.pyd +0 -0
  575. sqlspec/storage/pipeline.py +628 -0
  576. sqlspec/storage/registry.cp314-win_amd64.pyd +0 -0
  577. sqlspec/storage/registry.py +329 -0
  578. sqlspec/typing.py +405 -0
  579. sqlspec/utils/__init__.py +7 -0
  580. sqlspec/utils/arrow_helpers.py +384 -0
  581. sqlspec/utils/config_tools.cp314-win_amd64.pyd +0 -0
  582. sqlspec/utils/config_tools.py +314 -0
  583. sqlspec/utils/correlation.cp314-win_amd64.pyd +0 -0
  584. sqlspec/utils/correlation.py +134 -0
  585. sqlspec/utils/deprecation.cp314-win_amd64.pyd +0 -0
  586. sqlspec/utils/deprecation.py +157 -0
  587. sqlspec/utils/dispatch.cp314-win_amd64.pyd +0 -0
  588. sqlspec/utils/dispatch.py +101 -0
  589. sqlspec/utils/fixtures.cp314-win_amd64.pyd +0 -0
  590. sqlspec/utils/fixtures.py +260 -0
  591. sqlspec/utils/logging.cp314-win_amd64.pyd +0 -0
  592. sqlspec/utils/logging.py +251 -0
  593. sqlspec/utils/module_loader.py +306 -0
  594. sqlspec/utils/portal.cp314-win_amd64.pyd +0 -0
  595. sqlspec/utils/portal.py +377 -0
  596. sqlspec/utils/schema.cp314-win_amd64.pyd +0 -0
  597. sqlspec/utils/schema.py +1040 -0
  598. sqlspec/utils/serializers/__init__.py +30 -0
  599. sqlspec/utils/serializers/_json.cp314-win_amd64.pyd +0 -0
  600. sqlspec/utils/serializers/_json.py +415 -0
  601. sqlspec/utils/serializers/_numpy.cp314-win_amd64.pyd +0 -0
  602. sqlspec/utils/serializers/_numpy.py +65 -0
  603. sqlspec/utils/serializers/_schema.cp314-win_amd64.pyd +0 -0
  604. sqlspec/utils/serializers/_schema.py +285 -0
  605. sqlspec/utils/singleton.cp314-win_amd64.pyd +0 -0
  606. sqlspec/utils/singleton.py +41 -0
  607. sqlspec/utils/sync_tools.cp314-win_amd64.pyd +0 -0
  608. sqlspec/utils/sync_tools.py +316 -0
  609. sqlspec/utils/text.cp314-win_amd64.pyd +0 -0
  610. sqlspec/utils/text.py +109 -0
  611. sqlspec/utils/type_converters.cp314-win_amd64.pyd +0 -0
  612. sqlspec/utils/type_converters.py +216 -0
  613. sqlspec/utils/type_guards.cp314-win_amd64.pyd +0 -0
  614. sqlspec/utils/type_guards.py +1508 -0
  615. sqlspec/utils/uuids.cp314-win_amd64.pyd +0 -0
  616. sqlspec/utils/uuids.py +241 -0
  617. sqlspec-0.47.0.dist-info/METADATA +202 -0
  618. sqlspec-0.47.0.dist-info/RECORD +621 -0
  619. sqlspec-0.47.0.dist-info/WHEEL +4 -0
  620. sqlspec-0.47.0.dist-info/entry_points.txt +6 -0
  621. sqlspec-0.47.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,2453 @@
1
+ """Oracle ADK store for Google Agent Development Kit session/event storage."""
2
+
3
+ from decimal import Decimal
4
+ from enum import Enum
5
+ from typing import TYPE_CHECKING, Any, Final, cast
6
+
7
+ import oracledb
8
+
9
+ from sqlspec import SQL
10
+ from sqlspec.adapters.oracledb.data_dictionary import (
11
+ OracledbAsyncDataDictionary,
12
+ OracledbSyncDataDictionary,
13
+ OracleVersionInfo,
14
+ )
15
+ from sqlspec.extensions.adk import BaseAsyncADKStore, EventRecord, SessionRecord
16
+ from sqlspec.extensions.adk.memory.store import BaseAsyncADKMemoryStore
17
+ from sqlspec.utils.logging import get_logger
18
+ from sqlspec.utils.serializers import from_json, to_json
19
+ from sqlspec.utils.sync_tools import async_, run_
20
+ from sqlspec.utils.type_guards import is_async_readable, is_readable
21
+
22
+ if TYPE_CHECKING:
23
+ from datetime import datetime
24
+
25
+ from sqlspec.adapters.oracledb.config import OracleAsyncConfig, OracleSyncConfig
26
+ from sqlspec.extensions.adk import MemoryRecord
27
+
28
+ logger = get_logger("sqlspec.adapters.oracledb.adk.store")
29
+
30
+ __all__ = (
31
+ "JSONStorageType",
32
+ "OracleAsyncADKMemoryStore",
33
+ "OracleAsyncADKStore",
34
+ "OracleSyncADKMemoryStore",
35
+ "OracleSyncADKStore",
36
+ "coerce_decimal_values",
37
+ "storage_type_from_version",
38
+ )
39
+
40
+ ORACLE_TABLE_NOT_FOUND_ERROR: Final = 942
41
+ ORACLE_MIN_JSON_NATIVE_VERSION: Final = 21
42
+ ORACLE_MIN_JSON_NATIVE_COMPATIBLE: Final = 20
43
+ ORACLE_MIN_JSON_BLOB_VERSION: Final = 12
44
+ ORACLE_DEFAULT_HASH_PARTITIONS: Final = 16
45
+ ORACLE_MIN_HASH_PARTITIONS: Final = 2
46
+ ORACLE_RANGE_INTERVALS: Final[dict[str, str]] = {
47
+ "day": "NUMTODSINTERVAL(1, 'DAY')",
48
+ "week": "NUMTODSINTERVAL(7, 'DAY')",
49
+ "month": "NUMTOYMINTERVAL(1, 'MONTH')",
50
+ "year": "NUMTOYMINTERVAL(1, 'YEAR')",
51
+ }
52
+ ORACLE_COMPRESSION_CLAUSES: Final[dict[str, str]] = {
53
+ "basic": "ROW STORE COMPRESS BASIC",
54
+ "oltp": "ROW STORE COMPRESS ADVANCED",
55
+ "advanced": "ROW STORE COMPRESS ADVANCED",
56
+ "query_low": "COLUMN STORE COMPRESS FOR QUERY LOW",
57
+ "query_high": "COLUMN STORE COMPRESS FOR QUERY HIGH",
58
+ "archive_low": "COLUMN STORE COMPRESS FOR ARCHIVE LOW",
59
+ "archive_high": "COLUMN STORE COMPRESS FOR ARCHIVE HIGH",
60
+ }
61
+ ORACLE_DUPLICATE_KEY_ERROR: Final = 1
62
+
63
+
64
+ class JSONStorageType(str, Enum):
65
+ """JSON storage type based on Oracle version."""
66
+
67
+ JSON_NATIVE = "json"
68
+ BLOB_JSON = "blob_json"
69
+ BLOB_PLAIN = "blob_plain"
70
+
71
+
72
+ def coerce_decimal_values(value: Any) -> Any:
73
+ return _coerce_decimal_values(value)
74
+
75
+
76
+ def storage_type_from_version(version_info: "OracleVersionInfo | None") -> JSONStorageType:
77
+ return _storage_type_from_version(version_info)
78
+
79
+
80
+ class OracleAsyncADKStore(BaseAsyncADKStore["OracleAsyncConfig"]):
81
+ """Oracle async ADK store using oracledb async driver.
82
+
83
+ Implements session and event storage for Google Agent Development Kit
84
+ using Oracle Database via the python-oracledb async driver. Provides:
85
+ - Session state management with version-specific JSON storage
86
+ - Full-fidelity event storage via ``event_json`` column
87
+ - Atomic ``append_event_and_update_state`` for durable session mutations
88
+ - TIMESTAMP WITH TIME ZONE for timezone-aware timestamps
89
+ - Foreign key constraints with cascade delete
90
+ - Efficient upserts using MERGE statement
91
+
92
+ Args:
93
+ config: OracleAsyncConfig with extension_config["adk"] settings.
94
+
95
+ Notes:
96
+ - JSON storage type detected based on Oracle version (21c+, 12c+, legacy)
97
+ - event_json stored as JSON (21c+) or BLOB (older versions)
98
+ - TIMESTAMP WITH TIME ZONE for timezone-aware timestamps
99
+ - Named parameters using :param_name
100
+ - State merging handled at application level
101
+ - owner_id_column supports NUMBER, VARCHAR2, RAW for Oracle FK types
102
+ - Configuration is read from config.extension_config["adk"]
103
+ """
104
+
105
+ __slots__ = ("_in_memory", "_json_storage_type", "_oracle_version_info")
106
+
107
+ def __init__(self, config: "OracleAsyncConfig") -> None:
108
+ """Initialize Oracle ADK store.
109
+
110
+ Args:
111
+ config: OracleAsyncConfig instance.
112
+
113
+ Notes:
114
+ Configuration is read from config.extension_config["adk"]:
115
+ - session_table: Sessions table name (default: "adk_sessions")
116
+ - events_table: Events table name (default: "adk_events")
117
+ - owner_id_column: Optional owner FK column DDL (default: None)
118
+ - in_memory: Enable INMEMORY PRIORITY HIGH clause (default: False)
119
+ """
120
+ super().__init__(config)
121
+ self._json_storage_type: JSONStorageType | None = None
122
+ self._oracle_version_info: OracleVersionInfo | None = None
123
+
124
+ adk_config = config.extension_config.get("adk", {})
125
+ self._in_memory: bool = bool(adk_config.get("in_memory", False))
126
+
127
+ async def _get_create_sessions_table_sql(self) -> str:
128
+ """Get Oracle CREATE TABLE SQL for sessions table.
129
+
130
+ Auto-detects optimal JSON storage type based on Oracle version.
131
+ Result is cached to minimize database queries.
132
+ """
133
+ storage_type = await self._detect_json_storage_type()
134
+ return self._get_create_sessions_table_sql_for_type(storage_type)
135
+
136
+ async def _get_create_events_table_sql(self) -> str:
137
+ """Get Oracle CREATE TABLE SQL for events table.
138
+
139
+ Auto-detects optimal JSON storage type based on Oracle version.
140
+ Result is cached to minimize database queries.
141
+ """
142
+ storage_type = await self._detect_json_storage_type()
143
+ return self._get_create_events_table_sql_for_type(storage_type)
144
+
145
+ async def _detect_json_storage_type(self) -> JSONStorageType:
146
+ """Detect the appropriate JSON storage type based on Oracle version.
147
+
148
+ Returns:
149
+ Appropriate JSONStorageType for this Oracle version.
150
+
151
+ Notes:
152
+ Queries product_component_version to determine Oracle version.
153
+ - Oracle 21c+ with compatible >= 20: Native JSON type
154
+ - Oracle 12c+: BLOB with IS JSON constraint
155
+ - Oracle 11g and earlier: plain BLOB
156
+
157
+ Result is cached in self._json_storage_type.
158
+ """
159
+ if self._json_storage_type is not None:
160
+ return self._json_storage_type
161
+
162
+ version_info = await self._get_version_info()
163
+ self._json_storage_type = _storage_type_from_version(version_info)
164
+ return self._json_storage_type
165
+
166
+ async def _get_version_info(self) -> "OracleVersionInfo | None":
167
+ """Return cached Oracle version info using Oracle data dictionary."""
168
+
169
+ if self._oracle_version_info is not None:
170
+ return self._oracle_version_info
171
+
172
+ async with self._config.provide_session() as driver:
173
+ dictionary = OracledbAsyncDataDictionary()
174
+ self._oracle_version_info = await dictionary.get_version(driver)
175
+
176
+ if self._oracle_version_info is None:
177
+ logger.warning("Could not detect Oracle version, defaulting to BLOB_JSON storage")
178
+
179
+ return self._oracle_version_info
180
+
181
+ async def _serialize_state(self, state: "dict[str, Any]") -> "str | bytes":
182
+ """Serialize state dictionary to appropriate format based on storage type.
183
+
184
+ Args:
185
+ state: State dictionary to serialize.
186
+
187
+ Returns:
188
+ JSON string for JSON_NATIVE, bytes for BLOB types.
189
+ """
190
+ storage_type = await self._detect_json_storage_type()
191
+
192
+ if storage_type == JSONStorageType.JSON_NATIVE:
193
+ return to_json(state)
194
+
195
+ return to_json(state, as_bytes=True)
196
+
197
+ async def _deserialize_state(self, data: Any) -> "dict[str, Any]":
198
+ """Deserialize state data from database format.
199
+
200
+ Args:
201
+ data: Data from database (may be LOB, str, bytes, or dict).
202
+
203
+ Returns:
204
+ Deserialized state dictionary.
205
+
206
+ Notes:
207
+ Handles LOB reading if data has read() method.
208
+ Oracle JSON type may return dict directly.
209
+ """
210
+ if is_async_readable(data):
211
+ data = await data.read()
212
+ elif is_readable(data):
213
+ data = data.read()
214
+
215
+ if isinstance(data, dict):
216
+ return cast("dict[str, Any]", _coerce_decimal_values(data))
217
+
218
+ if isinstance(data, bytes):
219
+ return from_json(data) # type: ignore[no-any-return]
220
+
221
+ if isinstance(data, str):
222
+ return from_json(data) # type: ignore[no-any-return]
223
+
224
+ return from_json(str(data)) # type: ignore[no-any-return]
225
+
226
+ async def _deserialize_json_field(self, data: Any) -> "dict[str, Any] | None":
227
+ """Deserialize JSON payloads from Oracle JSON/BLOB/LOB values."""
228
+ if data is None:
229
+ return None
230
+ return await self._deserialize_state(data)
231
+
232
+ async def _serialize_event_json(self, event_json: Any) -> "str | bytes":
233
+ """Serialize event_json to the configured Oracle JSON storage format."""
234
+ storage_type = await self._detect_json_storage_type()
235
+ if storage_type == JSONStorageType.JSON_NATIVE:
236
+ return to_json(event_json)
237
+ return to_json(event_json, as_bytes=True)
238
+
239
+ async def _read_event_json(self, data: Any) -> str:
240
+ """Read event_json from database, handling LOB types.
241
+
242
+ Args:
243
+ data: Data from database (may be LOB, str, or dict).
244
+
245
+ Returns:
246
+ JSON string.
247
+ """
248
+ if is_async_readable(data):
249
+ data = await data.read()
250
+ elif is_readable(data):
251
+ data = data.read()
252
+
253
+ if isinstance(data, dict):
254
+ return to_json(data)
255
+
256
+ if isinstance(data, bytes):
257
+ return data.decode("utf-8")
258
+
259
+ return str(data)
260
+
261
+ def _get_create_sessions_table_sql_for_type(self, storage_type: JSONStorageType) -> str:
262
+ """Get Oracle CREATE TABLE SQL for sessions with specified storage type.
263
+
264
+ Args:
265
+ storage_type: JSON storage type to use.
266
+
267
+ Returns:
268
+ SQL statement to create adk_sessions table.
269
+ """
270
+ if storage_type == JSONStorageType.JSON_NATIVE:
271
+ state_column = "state JSON NOT NULL"
272
+ elif storage_type == JSONStorageType.BLOB_JSON:
273
+ state_column = "state BLOB CHECK (state IS JSON) NOT NULL"
274
+ else:
275
+ state_column = "state BLOB NOT NULL"
276
+
277
+ owner_id_column_sql = f", {self._owner_id_column_ddl}" if self._owner_id_column_ddl else ""
278
+ table_clauses = _oracle_table_feature_clauses(
279
+ self._config,
280
+ "session",
281
+ in_memory=self._in_memory,
282
+ hash_partition_key="id",
283
+ range_partition_key="create_time",
284
+ )
285
+
286
+ return f"""
287
+ BEGIN
288
+ EXECUTE IMMEDIATE 'CREATE TABLE {self._session_table} (
289
+ id VARCHAR2(128) PRIMARY KEY,
290
+ app_name VARCHAR2(128) NOT NULL,
291
+ user_id VARCHAR2(128) NOT NULL,
292
+ {state_column},
293
+ create_time TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL,
294
+ update_time TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL{owner_id_column_sql}
295
+ ){table_clauses}';
296
+ EXCEPTION
297
+ WHEN OTHERS THEN
298
+ IF SQLCODE != -955 THEN
299
+ RAISE;
300
+ END IF;
301
+ END;
302
+
303
+ BEGIN
304
+ EXECUTE IMMEDIATE 'CREATE INDEX idx_{self._session_table}_app_user
305
+ ON {self._session_table}(app_name, user_id)';
306
+ EXCEPTION
307
+ WHEN OTHERS THEN
308
+ IF SQLCODE != -955 THEN
309
+ RAISE;
310
+ END IF;
311
+ END;
312
+
313
+ BEGIN
314
+ EXECUTE IMMEDIATE 'CREATE INDEX idx_{self._session_table}_update_time
315
+ ON {self._session_table}(update_time DESC)';
316
+ EXCEPTION
317
+ WHEN OTHERS THEN
318
+ IF SQLCODE != -955 THEN
319
+ RAISE;
320
+ END IF;
321
+ END;
322
+ """
323
+
324
+ def _get_create_events_table_sql_for_type(self, storage_type: JSONStorageType) -> str:
325
+ """Get Oracle CREATE TABLE SQL for events with specified storage type.
326
+
327
+ The events table uses the new 5-column contract: session_id, invocation_id,
328
+ author, timestamp, and event_json. The event_json column stores the full
329
+ ADK Event as JSON (21c+) or BLOB (older versions).
330
+
331
+ Args:
332
+ storage_type: JSON storage type to use.
333
+
334
+ Returns:
335
+ SQL statement to create adk_events table.
336
+ """
337
+ event_json_col = _event_json_column_ddl(storage_type)
338
+ table_clauses = _oracle_table_feature_clauses(
339
+ self._config,
340
+ "events",
341
+ in_memory=self._in_memory,
342
+ hash_partition_key="session_id",
343
+ range_partition_key="timestamp",
344
+ )
345
+
346
+ return f"""
347
+ BEGIN
348
+ EXECUTE IMMEDIATE 'CREATE TABLE {self._events_table} (
349
+ session_id VARCHAR2(128) NOT NULL,
350
+ invocation_id VARCHAR2(256),
351
+ author VARCHAR2(256),
352
+ timestamp TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL,
353
+ {event_json_col},
354
+ CONSTRAINT fk_{self._events_table}_session FOREIGN KEY (session_id)
355
+ REFERENCES {self._session_table}(id) ON DELETE CASCADE
356
+ ){table_clauses}';
357
+ EXCEPTION
358
+ WHEN OTHERS THEN
359
+ IF SQLCODE != -955 THEN
360
+ RAISE;
361
+ END IF;
362
+ END;
363
+
364
+ BEGIN
365
+ EXECUTE IMMEDIATE 'CREATE INDEX idx_{self._events_table}_session
366
+ ON {self._events_table}(session_id, timestamp ASC)';
367
+ EXCEPTION
368
+ WHEN OTHERS THEN
369
+ IF SQLCODE != -955 THEN
370
+ RAISE;
371
+ END IF;
372
+ END;
373
+ """
374
+
375
+ def _get_drop_tables_sql(self) -> "list[str]":
376
+ """Get Oracle DROP TABLE SQL statements.
377
+
378
+ Returns:
379
+ List of SQL statements to drop tables and indexes.
380
+
381
+ Notes:
382
+ Order matters: drop events table (child) before sessions (parent).
383
+ Oracle automatically drops indexes when dropping tables.
384
+ """
385
+ return [
386
+ f"""
387
+ BEGIN
388
+ EXECUTE IMMEDIATE 'DROP INDEX idx_{self._events_table}_session';
389
+ EXCEPTION
390
+ WHEN OTHERS THEN
391
+ IF SQLCODE != -1418 THEN
392
+ RAISE;
393
+ END IF;
394
+ END;
395
+ """,
396
+ f"""
397
+ BEGIN
398
+ EXECUTE IMMEDIATE 'DROP INDEX idx_{self._session_table}_update_time';
399
+ EXCEPTION
400
+ WHEN OTHERS THEN
401
+ IF SQLCODE != -1418 THEN
402
+ RAISE;
403
+ END IF;
404
+ END;
405
+ """,
406
+ f"""
407
+ BEGIN
408
+ EXECUTE IMMEDIATE 'DROP INDEX idx_{self._session_table}_app_user';
409
+ EXCEPTION
410
+ WHEN OTHERS THEN
411
+ IF SQLCODE != -1418 THEN
412
+ RAISE;
413
+ END IF;
414
+ END;
415
+ """,
416
+ f"""
417
+ BEGIN
418
+ EXECUTE IMMEDIATE 'DROP TABLE {self._events_table}';
419
+ EXCEPTION
420
+ WHEN OTHERS THEN
421
+ IF SQLCODE != -942 THEN
422
+ RAISE;
423
+ END IF;
424
+ END;
425
+ """,
426
+ f"""
427
+ BEGIN
428
+ EXECUTE IMMEDIATE 'DROP TABLE {self._session_table}';
429
+ EXCEPTION
430
+ WHEN OTHERS THEN
431
+ IF SQLCODE != -942 THEN
432
+ RAISE;
433
+ END IF;
434
+ END;
435
+ """,
436
+ ]
437
+
438
+ async def create_tables(self) -> None:
439
+ """Create both sessions and events tables if they don't exist.
440
+
441
+ Notes:
442
+ Detects Oracle version to determine optimal JSON storage type.
443
+ Uses version-appropriate table schema.
444
+ """
445
+ storage_type = await self._detect_json_storage_type()
446
+ logger.debug("Creating ADK tables with storage type: %s", storage_type)
447
+
448
+ async with self._config.provide_session() as driver:
449
+ await driver.execute_script(self._get_create_sessions_table_sql_for_type(storage_type))
450
+
451
+ await driver.execute_script(self._get_create_events_table_sql_for_type(storage_type))
452
+
453
+ async def create_session(
454
+ self, session_id: str, app_name: str, user_id: str, state: "dict[str, Any]", owner_id: "Any | None" = None
455
+ ) -> SessionRecord:
456
+ """Create a new session.
457
+
458
+ Args:
459
+ session_id: Unique session identifier.
460
+ app_name: Application name.
461
+ user_id: User identifier.
462
+ state: Initial session state.
463
+ owner_id: Optional owner ID value for owner_id_column (if configured).
464
+
465
+ Returns:
466
+ Created session record.
467
+
468
+ Notes:
469
+ Uses SYSTIMESTAMP for create_time and update_time.
470
+ State is serialized using version-appropriate format.
471
+ owner_id is ignored if owner_id_column not configured.
472
+ """
473
+ state_data = await self._serialize_state(state)
474
+
475
+ if self._owner_id_column_name:
476
+ sql = f"""
477
+ INSERT INTO {self._session_table} (id, app_name, user_id, state, create_time, update_time, {self._owner_id_column_name})
478
+ VALUES (:id, :app_name, :user_id, :state, SYSTIMESTAMP, SYSTIMESTAMP, :owner_id)
479
+ """
480
+ params = {
481
+ "id": session_id,
482
+ "app_name": app_name,
483
+ "user_id": user_id,
484
+ "state": state_data,
485
+ "owner_id": owner_id,
486
+ }
487
+ else:
488
+ sql = f"""
489
+ INSERT INTO {self._session_table} (id, app_name, user_id, state, create_time, update_time)
490
+ VALUES (:id, :app_name, :user_id, :state, SYSTIMESTAMP, SYSTIMESTAMP)
491
+ """
492
+ params = {"id": session_id, "app_name": app_name, "user_id": user_id, "state": state_data}
493
+
494
+ async with self._config.provide_connection() as conn:
495
+ cursor = conn.cursor()
496
+ await cursor.execute(sql, params)
497
+ await conn.commit()
498
+
499
+ return await self.get_session(session_id) # type: ignore[return-value]
500
+
501
+ async def get_session(self, session_id: str) -> "SessionRecord | None":
502
+ """Get session by ID.
503
+
504
+ Args:
505
+ session_id: Session identifier.
506
+
507
+ Returns:
508
+ Session record or None if not found.
509
+
510
+ Notes:
511
+ Oracle returns datetime objects for TIMESTAMP columns.
512
+ State is deserialized using version-appropriate format.
513
+ """
514
+
515
+ try:
516
+ async with self._config.provide_connection() as conn:
517
+ cursor = conn.cursor()
518
+ await cursor.execute(
519
+ f"""
520
+ SELECT id, app_name, user_id, state, create_time, update_time
521
+ FROM {self._session_table}
522
+ WHERE id = :id
523
+ """,
524
+ {"id": session_id},
525
+ )
526
+ row = await cursor.fetchone()
527
+
528
+ if row is None:
529
+ return None
530
+
531
+ session_id_val, app_name, user_id, state_data, create_time, update_time = row
532
+
533
+ state = await self._deserialize_state(state_data)
534
+
535
+ return SessionRecord(
536
+ id=session_id_val,
537
+ app_name=app_name,
538
+ user_id=user_id,
539
+ state=state,
540
+ create_time=create_time,
541
+ update_time=update_time,
542
+ )
543
+ except oracledb.DatabaseError as e:
544
+ error_obj = e.args[0] if e.args else None
545
+ if error_obj and error_obj.code == ORACLE_TABLE_NOT_FOUND_ERROR:
546
+ return None
547
+ raise
548
+
549
+ async def update_session_state(self, session_id: str, state: "dict[str, Any]") -> None:
550
+ """Update session state.
551
+
552
+ Args:
553
+ session_id: Session identifier.
554
+ state: New state dictionary (replaces existing state).
555
+
556
+ Notes:
557
+ This replaces the entire state dictionary.
558
+ Updates update_time to current timestamp.
559
+ State is serialized using version-appropriate format.
560
+ """
561
+ state_data = await self._serialize_state(state)
562
+
563
+ sql = f"""
564
+ UPDATE {self._session_table}
565
+ SET state = :state, update_time = SYSTIMESTAMP
566
+ WHERE id = :id
567
+ """
568
+
569
+ async with self._config.provide_connection() as conn:
570
+ cursor = conn.cursor()
571
+ await cursor.execute(sql, {"state": state_data, "id": session_id})
572
+ await conn.commit()
573
+
574
+ async def delete_session(self, session_id: str) -> None:
575
+ """Delete session and all associated events (cascade).
576
+
577
+ Args:
578
+ session_id: Session identifier.
579
+
580
+ Notes:
581
+ Foreign key constraint ensures events are cascade-deleted.
582
+ """
583
+ sql = f"DELETE FROM {self._session_table} WHERE id = :id"
584
+
585
+ async with self._config.provide_connection() as conn:
586
+ cursor = conn.cursor()
587
+ await cursor.execute(sql, {"id": session_id})
588
+ await conn.commit()
589
+
590
+ async def list_sessions(self, app_name: str, user_id: str | None = None) -> "list[SessionRecord]":
591
+ """List sessions for an app, optionally filtered by user.
592
+
593
+ Args:
594
+ app_name: Application name.
595
+ user_id: User identifier. If None, lists all sessions for the app.
596
+
597
+ Returns:
598
+ List of session records ordered by update_time DESC.
599
+
600
+ Notes:
601
+ Uses composite index on (app_name, user_id) when user_id is provided.
602
+ State is deserialized using version-appropriate format.
603
+ """
604
+
605
+ if user_id is None:
606
+ sql = f"""
607
+ SELECT id, app_name, user_id, state, create_time, update_time
608
+ FROM {self._session_table}
609
+ WHERE app_name = :app_name
610
+ ORDER BY update_time DESC
611
+ """
612
+ params = {"app_name": app_name}
613
+ else:
614
+ sql = f"""
615
+ SELECT id, app_name, user_id, state, create_time, update_time
616
+ FROM {self._session_table}
617
+ WHERE app_name = :app_name AND user_id = :user_id
618
+ ORDER BY update_time DESC
619
+ """
620
+ params = {"app_name": app_name, "user_id": user_id}
621
+
622
+ try:
623
+ async with self._config.provide_connection() as conn:
624
+ cursor = conn.cursor()
625
+ await cursor.execute(sql, params)
626
+ rows = await cursor.fetchall()
627
+
628
+ results = []
629
+ for row in rows:
630
+ state = await self._deserialize_state(row[3])
631
+
632
+ results.append(
633
+ SessionRecord(
634
+ id=row[0],
635
+ app_name=row[1],
636
+ user_id=row[2],
637
+ state=state,
638
+ create_time=row[4],
639
+ update_time=row[5],
640
+ )
641
+ )
642
+ return results
643
+ except oracledb.DatabaseError as e:
644
+ error_obj = e.args[0] if e.args else None
645
+ if error_obj and error_obj.code == ORACLE_TABLE_NOT_FOUND_ERROR:
646
+ return []
647
+ raise
648
+
649
+ async def append_event(self, event_record: EventRecord) -> None:
650
+ """Append an event to a session.
651
+
652
+ Args:
653
+ event_record: Event record with 5 keys: session_id, invocation_id,
654
+ author, timestamp, event_json.
655
+ """
656
+ sql = f"""
657
+ INSERT INTO {self._events_table} (
658
+ session_id, invocation_id, author, timestamp, event_json
659
+ ) VALUES (
660
+ :session_id, :invocation_id, :author, :timestamp, :event_json
661
+ )
662
+ """
663
+
664
+ async with self._config.provide_connection() as conn:
665
+ cursor = conn.cursor()
666
+ await cursor.execute(
667
+ sql,
668
+ {
669
+ "session_id": event_record["session_id"],
670
+ "invocation_id": event_record["invocation_id"],
671
+ "author": event_record["author"],
672
+ "timestamp": event_record["timestamp"],
673
+ "event_json": await self._serialize_event_json(event_record["event_json"]),
674
+ },
675
+ )
676
+ await conn.commit()
677
+
678
+ async def append_event_and_update_state(
679
+ self, event_record: EventRecord, session_id: str, state: "dict[str, Any]"
680
+ ) -> SessionRecord:
681
+ """Atomically append an event and update the session's durable state.
682
+
683
+ Both the event insert and session state update are executed within a
684
+ single transaction so they succeed or fail together. The refreshed
685
+ SessionRecord is read inside the same transaction (Oracle's RETURNING
686
+ INTO requires output bind variables which complicate async cursor
687
+ handling, so a SELECT-after-UPDATE is used instead).
688
+
689
+ Args:
690
+ event_record: Event record with 5 keys: session_id, invocation_id,
691
+ author, timestamp, event_json.
692
+ session_id: Session identifier whose state should be updated.
693
+ state: Post-append durable state snapshot (``temp:`` keys already
694
+ stripped by the service layer).
695
+ """
696
+ insert_sql = f"""
697
+ INSERT INTO {self._events_table} (
698
+ session_id, invocation_id, author, timestamp, event_json
699
+ ) VALUES (
700
+ :session_id, :invocation_id, :author, :timestamp, :event_json
701
+ )
702
+ """
703
+
704
+ state_data = await self._serialize_state(state)
705
+ update_sql = f"""
706
+ UPDATE {self._session_table}
707
+ SET state = :state, update_time = SYSTIMESTAMP
708
+ WHERE id = :id
709
+ """
710
+
711
+ select_sql = f"""
712
+ SELECT id, app_name, user_id, state, create_time, update_time
713
+ FROM {self._session_table}
714
+ WHERE id = :id
715
+ """
716
+
717
+ async with self._config.provide_connection() as conn:
718
+ cursor = conn.cursor()
719
+ await cursor.execute(
720
+ insert_sql,
721
+ {
722
+ "session_id": event_record["session_id"],
723
+ "invocation_id": event_record["invocation_id"],
724
+ "author": event_record["author"],
725
+ "timestamp": event_record["timestamp"],
726
+ "event_json": await self._serialize_event_json(event_record["event_json"]),
727
+ },
728
+ )
729
+ await cursor.execute(update_sql, {"state": state_data, "id": session_id})
730
+ await cursor.execute(select_sql, {"id": session_id})
731
+ row = await cursor.fetchone()
732
+ await conn.commit()
733
+
734
+ if row is None:
735
+ msg = f"Session {session_id} not found during append_event_and_update_state."
736
+ raise ValueError(msg)
737
+
738
+ session_id_val, app_name, user_id, state_data_row, create_time, update_time = row
739
+ return SessionRecord(
740
+ id=session_id_val,
741
+ app_name=app_name,
742
+ user_id=user_id,
743
+ state=await self._deserialize_state(state_data_row),
744
+ create_time=create_time,
745
+ update_time=update_time,
746
+ )
747
+
748
+ async def get_events(
749
+ self, session_id: str, after_timestamp: "datetime | None" = None, limit: "int | None" = None
750
+ ) -> "list[EventRecord]":
751
+ """Get events for a session.
752
+
753
+ Args:
754
+ session_id: Session identifier.
755
+ after_timestamp: Only return events after this time.
756
+ limit: Maximum number of events to return.
757
+
758
+ Returns:
759
+ List of event records ordered by timestamp ASC.
760
+ """
761
+
762
+ where_clauses = ["session_id = :session_id"]
763
+ params: dict[str, Any] = {"session_id": session_id}
764
+
765
+ if after_timestamp is not None:
766
+ where_clauses.append("timestamp > :after_timestamp")
767
+ params["after_timestamp"] = after_timestamp
768
+
769
+ where_clause = " AND ".join(where_clauses)
770
+ limit_clause = ""
771
+ if limit:
772
+ limit_clause = f" FETCH FIRST {limit} ROWS ONLY"
773
+
774
+ sql = f"""
775
+ SELECT session_id, invocation_id, author, timestamp, event_json
776
+ FROM {self._events_table}
777
+ WHERE {where_clause}
778
+ ORDER BY timestamp ASC{limit_clause}
779
+ """
780
+
781
+ try:
782
+ async with self._config.provide_connection() as conn:
783
+ cursor = conn.cursor()
784
+ await cursor.execute(sql, params)
785
+ rows = await cursor.fetchall()
786
+
787
+ return [
788
+ EventRecord(
789
+ session_id=row[0],
790
+ invocation_id=_oracle_text_value(row[1]),
791
+ author=_oracle_text_value(row[2]),
792
+ timestamp=row[3],
793
+ event_json=await self._deserialize_json_field(row[4]) or {},
794
+ )
795
+ for row in rows
796
+ ]
797
+ except oracledb.DatabaseError as e:
798
+ error_obj = e.args[0] if e.args else None
799
+ if error_obj and error_obj.code == ORACLE_TABLE_NOT_FOUND_ERROR:
800
+ return []
801
+ raise
802
+
803
+
804
+ class OracleSyncADKStore(BaseAsyncADKStore["OracleSyncConfig"]):
805
+ """Oracle synchronous ADK store using oracledb sync driver.
806
+
807
+ Implements session and event storage for Google Agent Development Kit
808
+ using Oracle Database via the python-oracledb synchronous driver. Provides:
809
+ - Session state management with version-specific JSON storage
810
+ - Full-fidelity event storage via ``event_json`` column
811
+ - Atomic ``create_event_and_update_state`` for durable session mutations
812
+ - TIMESTAMP WITH TIME ZONE for timezone-aware timestamps
813
+ - Foreign key constraints with cascade delete
814
+ - Efficient upserts using MERGE statement
815
+
816
+ Args:
817
+ config: OracleSyncConfig with extension_config["adk"] settings.
818
+
819
+ Notes:
820
+ - JSON storage type detected based on Oracle version (21c+, 12c+, legacy)
821
+ - event_json stored as JSON (21c+) or BLOB (older versions)
822
+ - TIMESTAMP WITH TIME ZONE for timezone-aware timestamps
823
+ - Named parameters using :param_name
824
+ - State merging handled at application level
825
+ - owner_id_column supports NUMBER, VARCHAR2, RAW for Oracle FK types
826
+ - Configuration is read from config.extension_config["adk"]
827
+ """
828
+
829
+ __slots__ = ("_in_memory", "_json_storage_type", "_oracle_version_info")
830
+
831
+ def __init__(self, config: "OracleSyncConfig") -> None:
832
+ """Initialize Oracle synchronous ADK store.
833
+
834
+ Args:
835
+ config: OracleSyncConfig instance.
836
+
837
+ Notes:
838
+ Configuration is read from config.extension_config["adk"]:
839
+ - session_table: Sessions table name (default: "adk_sessions")
840
+ - events_table: Events table name (default: "adk_events")
841
+ - owner_id_column: Optional owner FK column DDL (default: None)
842
+ - in_memory: Enable INMEMORY PRIORITY HIGH clause (default: False)
843
+ """
844
+ super().__init__(config)
845
+ self._json_storage_type: JSONStorageType | None = None
846
+ self._oracle_version_info: OracleVersionInfo | None = None
847
+
848
+ adk_config = config.extension_config.get("adk", {})
849
+ self._in_memory: bool = bool(adk_config.get("in_memory", False))
850
+
851
+ async def _get_create_sessions_table_sql(self) -> str:
852
+ """Get Oracle CREATE TABLE SQL for sessions table.
853
+
854
+ Auto-detects optimal JSON storage type based on Oracle version.
855
+ Result is cached to minimize database queries.
856
+ """
857
+ storage_type = self._detect_json_storage_type()
858
+ return self._get_create_sessions_table_sql_for_type(storage_type)
859
+
860
+ async def _get_create_events_table_sql(self) -> str:
861
+ """Get Oracle CREATE TABLE SQL for events table.
862
+
863
+ Auto-detects optimal JSON storage type based on Oracle version.
864
+ Result is cached to minimize database queries.
865
+ """
866
+ storage_type = self._detect_json_storage_type()
867
+ return self._get_create_events_table_sql_for_type(storage_type)
868
+
869
+ def _detect_json_storage_type(self) -> JSONStorageType:
870
+ """Detect the appropriate JSON storage type based on Oracle version.
871
+
872
+ Returns:
873
+ Appropriate JSONStorageType for this Oracle version.
874
+
875
+ Notes:
876
+ Queries product_component_version to determine Oracle version.
877
+ - Oracle 21c+ with compatible >= 20: Native JSON type
878
+ - Oracle 12c+: BLOB with IS JSON constraint
879
+ - Oracle 11g and earlier: plain BLOB
880
+
881
+ Result is cached in self._json_storage_type.
882
+ """
883
+ if self._json_storage_type is not None:
884
+ return self._json_storage_type
885
+
886
+ version_info = self._get_version_info()
887
+ self._json_storage_type = _storage_type_from_version(version_info)
888
+ return self._json_storage_type
889
+
890
+ def _get_version_info(self) -> "OracleVersionInfo | None":
891
+ """Return cached Oracle version info using Oracle data dictionary."""
892
+
893
+ if self._oracle_version_info is not None:
894
+ return self._oracle_version_info
895
+
896
+ with self._config.provide_session() as driver:
897
+ dictionary = OracledbSyncDataDictionary()
898
+ self._oracle_version_info = dictionary.get_version(driver)
899
+
900
+ if self._oracle_version_info is None:
901
+ logger.warning("Could not detect Oracle version, defaulting to BLOB_JSON storage")
902
+
903
+ return self._oracle_version_info
904
+
905
+ def _serialize_state(self, state: "dict[str, Any]") -> "str | bytes":
906
+ """Serialize state dictionary to appropriate format based on storage type.
907
+
908
+ Args:
909
+ state: State dictionary to serialize.
910
+
911
+ Returns:
912
+ JSON string for JSON_NATIVE, bytes for BLOB types.
913
+ """
914
+ storage_type = self._detect_json_storage_type()
915
+
916
+ if storage_type == JSONStorageType.JSON_NATIVE:
917
+ return to_json(state)
918
+
919
+ return to_json(state, as_bytes=True)
920
+
921
+ def _deserialize_state(self, data: Any) -> "dict[str, Any]":
922
+ """Deserialize state data from database format.
923
+
924
+ Args:
925
+ data: Data from database (may be LOB, str, bytes, or dict).
926
+
927
+ Returns:
928
+ Deserialized state dictionary.
929
+
930
+ Notes:
931
+ Handles LOB reading if data has read() method.
932
+ Oracle JSON type may return dict directly.
933
+ """
934
+ if is_readable(data):
935
+ data = data.read()
936
+
937
+ if isinstance(data, dict):
938
+ return cast("dict[str, Any]", _coerce_decimal_values(data))
939
+
940
+ if isinstance(data, bytes):
941
+ return from_json(data) # type: ignore[no-any-return]
942
+
943
+ if isinstance(data, str):
944
+ return from_json(data) # type: ignore[no-any-return]
945
+
946
+ return from_json(str(data)) # type: ignore[no-any-return]
947
+
948
+ def _deserialize_json_field(self, data: Any) -> "dict[str, Any] | None":
949
+ """Deserialize JSON payloads from Oracle JSON/BLOB/LOB values."""
950
+ if data is None:
951
+ return None
952
+ return self._deserialize_state(data)
953
+
954
+ def _serialize_event_json(self, event_json: Any) -> "str | bytes":
955
+ """Serialize event_json to the configured Oracle JSON storage format."""
956
+ storage_type = self._detect_json_storage_type()
957
+ if storage_type == JSONStorageType.JSON_NATIVE:
958
+ return to_json(event_json)
959
+ return to_json(event_json, as_bytes=True)
960
+
961
+ def _read_event_json(self, data: Any) -> str:
962
+ """Read event_json from database, handling LOB types.
963
+
964
+ Args:
965
+ data: Data from database (may be LOB, str, or dict).
966
+
967
+ Returns:
968
+ JSON string.
969
+ """
970
+ if is_readable(data):
971
+ data = data.read()
972
+
973
+ if isinstance(data, dict):
974
+ return to_json(data)
975
+
976
+ if isinstance(data, bytes):
977
+ return data.decode("utf-8")
978
+
979
+ return str(data)
980
+
981
+ def _get_create_sessions_table_sql_for_type(self, storage_type: JSONStorageType) -> str:
982
+ """Get Oracle CREATE TABLE SQL for sessions with specified storage type.
983
+
984
+ Args:
985
+ storage_type: JSON storage type to use.
986
+
987
+ Returns:
988
+ SQL statement to create adk_sessions table.
989
+ """
990
+ if storage_type == JSONStorageType.JSON_NATIVE:
991
+ state_column = "state JSON NOT NULL"
992
+ elif storage_type == JSONStorageType.BLOB_JSON:
993
+ state_column = "state BLOB CHECK (state IS JSON) NOT NULL"
994
+ else:
995
+ state_column = "state BLOB NOT NULL"
996
+
997
+ owner_id_column_sql = f", {self._owner_id_column_ddl}" if self._owner_id_column_ddl else ""
998
+ table_clauses = _oracle_table_feature_clauses(
999
+ self._config,
1000
+ "session",
1001
+ in_memory=self._in_memory,
1002
+ hash_partition_key="id",
1003
+ range_partition_key="create_time",
1004
+ )
1005
+
1006
+ return f"""
1007
+ BEGIN
1008
+ EXECUTE IMMEDIATE 'CREATE TABLE {self._session_table} (
1009
+ id VARCHAR2(128) PRIMARY KEY,
1010
+ app_name VARCHAR2(128) NOT NULL,
1011
+ user_id VARCHAR2(128) NOT NULL,
1012
+ {state_column},
1013
+ create_time TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL,
1014
+ update_time TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL{owner_id_column_sql}
1015
+ ){table_clauses}';
1016
+ EXCEPTION
1017
+ WHEN OTHERS THEN
1018
+ IF SQLCODE != -955 THEN
1019
+ RAISE;
1020
+ END IF;
1021
+ END;
1022
+
1023
+ BEGIN
1024
+ EXECUTE IMMEDIATE 'CREATE INDEX idx_{self._session_table}_app_user
1025
+ ON {self._session_table}(app_name, user_id)';
1026
+ EXCEPTION
1027
+ WHEN OTHERS THEN
1028
+ IF SQLCODE != -955 THEN
1029
+ RAISE;
1030
+ END IF;
1031
+ END;
1032
+
1033
+ BEGIN
1034
+ EXECUTE IMMEDIATE 'CREATE INDEX idx_{self._session_table}_update_time
1035
+ ON {self._session_table}(update_time DESC)';
1036
+ EXCEPTION
1037
+ WHEN OTHERS THEN
1038
+ IF SQLCODE != -955 THEN
1039
+ RAISE;
1040
+ END IF;
1041
+ END;
1042
+ """
1043
+
1044
+ def _get_create_events_table_sql_for_type(self, storage_type: JSONStorageType) -> str:
1045
+ """Get Oracle CREATE TABLE SQL for events with specified storage type.
1046
+
1047
+ The events table uses the new 5-column contract: session_id, invocation_id,
1048
+ author, timestamp, and event_json. The event_json column stores the full
1049
+ ADK Event as JSON (21c+) or BLOB (older versions).
1050
+
1051
+ Args:
1052
+ storage_type: JSON storage type to use.
1053
+
1054
+ Returns:
1055
+ SQL statement to create adk_events table.
1056
+ """
1057
+ event_json_col = _event_json_column_ddl(storage_type)
1058
+ table_clauses = _oracle_table_feature_clauses(
1059
+ self._config,
1060
+ "events",
1061
+ in_memory=self._in_memory,
1062
+ hash_partition_key="session_id",
1063
+ range_partition_key="timestamp",
1064
+ )
1065
+
1066
+ return f"""
1067
+ BEGIN
1068
+ EXECUTE IMMEDIATE 'CREATE TABLE {self._events_table} (
1069
+ session_id VARCHAR2(128) NOT NULL,
1070
+ invocation_id VARCHAR2(256),
1071
+ author VARCHAR2(256),
1072
+ timestamp TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL,
1073
+ {event_json_col},
1074
+ CONSTRAINT fk_{self._events_table}_session FOREIGN KEY (session_id)
1075
+ REFERENCES {self._session_table}(id) ON DELETE CASCADE
1076
+ ){table_clauses}';
1077
+ EXCEPTION
1078
+ WHEN OTHERS THEN
1079
+ IF SQLCODE != -955 THEN
1080
+ RAISE;
1081
+ END IF;
1082
+ END;
1083
+
1084
+ BEGIN
1085
+ EXECUTE IMMEDIATE 'CREATE INDEX idx_{self._events_table}_session
1086
+ ON {self._events_table}(session_id, timestamp ASC)';
1087
+ EXCEPTION
1088
+ WHEN OTHERS THEN
1089
+ IF SQLCODE != -955 THEN
1090
+ RAISE;
1091
+ END IF;
1092
+ END;
1093
+ """
1094
+
1095
+ def _get_drop_tables_sql(self) -> "list[str]":
1096
+ """Get Oracle DROP TABLE SQL statements.
1097
+
1098
+ Returns:
1099
+ List of SQL statements to drop tables and indexes.
1100
+
1101
+ Notes:
1102
+ Order matters: drop events table (child) before sessions (parent).
1103
+ Oracle automatically drops indexes when dropping tables.
1104
+ """
1105
+ return [
1106
+ f"""
1107
+ BEGIN
1108
+ EXECUTE IMMEDIATE 'DROP INDEX idx_{self._events_table}_session';
1109
+ EXCEPTION
1110
+ WHEN OTHERS THEN
1111
+ IF SQLCODE != -1418 THEN
1112
+ RAISE;
1113
+ END IF;
1114
+ END;
1115
+ """,
1116
+ f"""
1117
+ BEGIN
1118
+ EXECUTE IMMEDIATE 'DROP INDEX idx_{self._session_table}_update_time';
1119
+ EXCEPTION
1120
+ WHEN OTHERS THEN
1121
+ IF SQLCODE != -1418 THEN
1122
+ RAISE;
1123
+ END IF;
1124
+ END;
1125
+ """,
1126
+ f"""
1127
+ BEGIN
1128
+ EXECUTE IMMEDIATE 'DROP INDEX idx_{self._session_table}_app_user';
1129
+ EXCEPTION
1130
+ WHEN OTHERS THEN
1131
+ IF SQLCODE != -1418 THEN
1132
+ RAISE;
1133
+ END IF;
1134
+ END;
1135
+ """,
1136
+ f"""
1137
+ BEGIN
1138
+ EXECUTE IMMEDIATE 'DROP TABLE {self._events_table}';
1139
+ EXCEPTION
1140
+ WHEN OTHERS THEN
1141
+ IF SQLCODE != -942 THEN
1142
+ RAISE;
1143
+ END IF;
1144
+ END;
1145
+ """,
1146
+ f"""
1147
+ BEGIN
1148
+ EXECUTE IMMEDIATE 'DROP TABLE {self._session_table}';
1149
+ EXCEPTION
1150
+ WHEN OTHERS THEN
1151
+ IF SQLCODE != -942 THEN
1152
+ RAISE;
1153
+ END IF;
1154
+ END;
1155
+ """,
1156
+ ]
1157
+
1158
+ def _create_tables(self) -> None:
1159
+ """Create both sessions and events tables if they don't exist.
1160
+
1161
+ Notes:
1162
+ Detects Oracle version to determine optimal JSON storage type.
1163
+ Uses version-appropriate table schema.
1164
+ """
1165
+ storage_type = self._detect_json_storage_type()
1166
+ logger.info("Creating ADK tables with storage type: %s", storage_type)
1167
+
1168
+ with self._config.provide_session() as driver:
1169
+ sessions_sql = SQL(self._get_create_sessions_table_sql_for_type(storage_type))
1170
+ driver.execute_script(sessions_sql)
1171
+
1172
+ events_sql = SQL(self._get_create_events_table_sql_for_type(storage_type))
1173
+ driver.execute_script(events_sql)
1174
+
1175
+ async def create_tables(self) -> None:
1176
+ """Create tables if they don't exist."""
1177
+ await async_(self._create_tables)()
1178
+
1179
+ def _create_session(
1180
+ self, session_id: str, app_name: str, user_id: str, state: "dict[str, Any]", owner_id: "Any | None" = None
1181
+ ) -> SessionRecord:
1182
+ """Create a new session.
1183
+
1184
+ Args:
1185
+ session_id: Unique session identifier.
1186
+ app_name: Application name.
1187
+ user_id: User identifier.
1188
+ state: Initial session state.
1189
+ owner_id: Optional owner ID value for owner_id_column (if configured).
1190
+
1191
+ Returns:
1192
+ Created session record.
1193
+
1194
+ Notes:
1195
+ Uses SYSTIMESTAMP for create_time and update_time.
1196
+ State is serialized using version-appropriate format.
1197
+ owner_id is ignored if owner_id_column not configured.
1198
+ """
1199
+ state_data = self._serialize_state(state)
1200
+
1201
+ if self._owner_id_column_name:
1202
+ sql = f"""
1203
+ INSERT INTO {self._session_table} (id, app_name, user_id, state, create_time, update_time, {self._owner_id_column_name})
1204
+ VALUES (:id, :app_name, :user_id, :state, SYSTIMESTAMP, SYSTIMESTAMP, :owner_id)
1205
+ """
1206
+ params = {
1207
+ "id": session_id,
1208
+ "app_name": app_name,
1209
+ "user_id": user_id,
1210
+ "state": state_data,
1211
+ "owner_id": owner_id,
1212
+ }
1213
+ else:
1214
+ sql = f"""
1215
+ INSERT INTO {self._session_table} (id, app_name, user_id, state, create_time, update_time)
1216
+ VALUES (:id, :app_name, :user_id, :state, SYSTIMESTAMP, SYSTIMESTAMP)
1217
+ """
1218
+ params = {"id": session_id, "app_name": app_name, "user_id": user_id, "state": state_data}
1219
+
1220
+ with self._config.provide_connection() as conn:
1221
+ cursor = conn.cursor()
1222
+ cursor.execute(sql, params)
1223
+ conn.commit()
1224
+
1225
+ result = self._get_session(session_id)
1226
+ if result is None:
1227
+ msg = "Failed to fetch created session"
1228
+ raise RuntimeError(msg)
1229
+ return result
1230
+
1231
+ async def create_session(
1232
+ self, session_id: str, app_name: str, user_id: str, state: "dict[str, Any]", owner_id: "Any | None" = None
1233
+ ) -> SessionRecord:
1234
+ """Create a new session."""
1235
+ return await async_(self._create_session)(session_id, app_name, user_id, state, owner_id)
1236
+
1237
+ def _get_session(self, session_id: str) -> "SessionRecord | None":
1238
+ """Get session by ID.
1239
+
1240
+ Args:
1241
+ session_id: Session identifier.
1242
+
1243
+ Returns:
1244
+ Session record or None if not found.
1245
+
1246
+ Notes:
1247
+ Oracle returns datetime objects for TIMESTAMP columns.
1248
+ State is deserialized using version-appropriate format.
1249
+ """
1250
+
1251
+ sql = f"""
1252
+ SELECT id, app_name, user_id, state, create_time, update_time
1253
+ FROM {self._session_table}
1254
+ WHERE id = :id
1255
+ """
1256
+
1257
+ try:
1258
+ with self._config.provide_connection() as conn:
1259
+ cursor = conn.cursor()
1260
+ cursor.execute(sql, {"id": session_id})
1261
+ row = cursor.fetchone()
1262
+
1263
+ if row is None:
1264
+ return None
1265
+
1266
+ session_id_val, app_name, user_id, state_data, create_time, update_time = row
1267
+
1268
+ state = self._deserialize_state(state_data)
1269
+
1270
+ return SessionRecord(
1271
+ id=session_id_val,
1272
+ app_name=app_name,
1273
+ user_id=user_id,
1274
+ state=state,
1275
+ create_time=create_time,
1276
+ update_time=update_time,
1277
+ )
1278
+ except oracledb.DatabaseError as e:
1279
+ error_obj = e.args[0] if e.args else None
1280
+ if error_obj and error_obj.code == ORACLE_TABLE_NOT_FOUND_ERROR:
1281
+ return None
1282
+ raise
1283
+
1284
+ async def get_session(self, session_id: str) -> "SessionRecord | None":
1285
+ """Get session by ID."""
1286
+ return await async_(self._get_session)(session_id)
1287
+
1288
+ def _update_session_state(self, session_id: str, state: "dict[str, Any]") -> None:
1289
+ """Update session state.
1290
+
1291
+ Args:
1292
+ session_id: Session identifier.
1293
+ state: New state dictionary (replaces existing state).
1294
+
1295
+ Notes:
1296
+ This replaces the entire state dictionary.
1297
+ Updates update_time to current timestamp.
1298
+ State is serialized using version-appropriate format.
1299
+ """
1300
+ state_data = self._serialize_state(state)
1301
+
1302
+ sql = f"""
1303
+ UPDATE {self._session_table}
1304
+ SET state = :state, update_time = SYSTIMESTAMP
1305
+ WHERE id = :id
1306
+ """
1307
+
1308
+ with self._config.provide_connection() as conn:
1309
+ cursor = conn.cursor()
1310
+ cursor.execute(sql, {"state": state_data, "id": session_id})
1311
+ conn.commit()
1312
+
1313
+ async def update_session_state(self, session_id: str, state: "dict[str, Any]") -> None:
1314
+ """Update session state."""
1315
+ await async_(self._update_session_state)(session_id, state)
1316
+
1317
+ def _delete_session(self, session_id: str) -> None:
1318
+ """Delete session and all associated events (cascade).
1319
+
1320
+ Args:
1321
+ session_id: Session identifier.
1322
+
1323
+ Notes:
1324
+ Foreign key constraint ensures events are cascade-deleted.
1325
+ """
1326
+ sql = f"DELETE FROM {self._session_table} WHERE id = :id"
1327
+
1328
+ with self._config.provide_connection() as conn:
1329
+ cursor = conn.cursor()
1330
+ cursor.execute(sql, {"id": session_id})
1331
+ conn.commit()
1332
+
1333
+ async def delete_session(self, session_id: str) -> None:
1334
+ """Delete session and associated events."""
1335
+ await async_(self._delete_session)(session_id)
1336
+
1337
+ def _list_sessions(self, app_name: str, user_id: str | None = None) -> "list[SessionRecord]":
1338
+ """List sessions for an app, optionally filtered by user.
1339
+
1340
+ Args:
1341
+ app_name: Application name.
1342
+ user_id: User identifier. If None, lists all sessions for the app.
1343
+
1344
+ Returns:
1345
+ List of session records ordered by update_time DESC.
1346
+
1347
+ Notes:
1348
+ Uses composite index on (app_name, user_id) when user_id is provided.
1349
+ State is deserialized using version-appropriate format.
1350
+ """
1351
+
1352
+ if user_id is None:
1353
+ sql = f"""
1354
+ SELECT id, app_name, user_id, state, create_time, update_time
1355
+ FROM {self._session_table}
1356
+ WHERE app_name = :app_name
1357
+ ORDER BY update_time DESC
1358
+ """
1359
+ params = {"app_name": app_name}
1360
+ else:
1361
+ sql = f"""
1362
+ SELECT id, app_name, user_id, state, create_time, update_time
1363
+ FROM {self._session_table}
1364
+ WHERE app_name = :app_name AND user_id = :user_id
1365
+ ORDER BY update_time DESC
1366
+ """
1367
+ params = {"app_name": app_name, "user_id": user_id}
1368
+
1369
+ try:
1370
+ with self._config.provide_connection() as conn:
1371
+ cursor = conn.cursor()
1372
+ cursor.execute(sql, params)
1373
+ rows = cursor.fetchall()
1374
+
1375
+ results = []
1376
+ for row in rows:
1377
+ state = self._deserialize_state(row[3])
1378
+
1379
+ results.append(
1380
+ SessionRecord(
1381
+ id=row[0],
1382
+ app_name=row[1],
1383
+ user_id=row[2],
1384
+ state=state,
1385
+ create_time=row[4],
1386
+ update_time=row[5],
1387
+ )
1388
+ )
1389
+ return results
1390
+ except oracledb.DatabaseError as e:
1391
+ error_obj = e.args[0] if e.args else None
1392
+ if error_obj and error_obj.code == ORACLE_TABLE_NOT_FOUND_ERROR:
1393
+ return []
1394
+ raise
1395
+
1396
+ async def list_sessions(self, app_name: str, user_id: str | None = None) -> "list[SessionRecord]":
1397
+ """List sessions for an app."""
1398
+ return await async_(self._list_sessions)(app_name, user_id)
1399
+
1400
+ def _append_event_and_update_state(
1401
+ self, event_record: EventRecord, session_id: str, state: "dict[str, Any]"
1402
+ ) -> SessionRecord:
1403
+ """Atomically create an event and update the session's durable state.
1404
+
1405
+ Both the event insert and session state update are executed within a
1406
+ single transaction so they succeed or fail together; the refreshed
1407
+ SessionRecord is read inside the same transaction.
1408
+
1409
+ Args:
1410
+ event_record: Event record with 5 keys: session_id, invocation_id,
1411
+ author, timestamp, event_json.
1412
+ session_id: Session identifier whose state should be updated.
1413
+ state: Post-append durable state snapshot (``temp:`` keys already
1414
+ stripped by the service layer).
1415
+ """
1416
+ insert_sql = f"""
1417
+ INSERT INTO {self._events_table} (
1418
+ session_id, invocation_id, author, timestamp, event_json
1419
+ ) VALUES (
1420
+ :session_id, :invocation_id, :author, :timestamp, :event_json
1421
+ )
1422
+ """
1423
+
1424
+ state_data = self._serialize_state(state)
1425
+ update_sql = f"""
1426
+ UPDATE {self._session_table}
1427
+ SET state = :state, update_time = SYSTIMESTAMP
1428
+ WHERE id = :id
1429
+ """
1430
+
1431
+ select_sql = f"""
1432
+ SELECT id, app_name, user_id, state, create_time, update_time
1433
+ FROM {self._session_table}
1434
+ WHERE id = :id
1435
+ """
1436
+
1437
+ with self._config.provide_connection() as conn:
1438
+ cursor = conn.cursor()
1439
+ cursor.execute(
1440
+ insert_sql,
1441
+ {
1442
+ "session_id": event_record["session_id"],
1443
+ "invocation_id": event_record["invocation_id"],
1444
+ "author": event_record["author"],
1445
+ "timestamp": event_record["timestamp"],
1446
+ "event_json": self._serialize_event_json(event_record["event_json"]),
1447
+ },
1448
+ )
1449
+ cursor.execute(update_sql, {"state": state_data, "id": session_id})
1450
+ cursor.execute(select_sql, {"id": session_id})
1451
+ row = cursor.fetchone()
1452
+ conn.commit()
1453
+
1454
+ if row is None:
1455
+ msg = f"Session {session_id} not found during append_event_and_update_state."
1456
+ raise ValueError(msg)
1457
+
1458
+ session_id_val, app_name, user_id, state_data_row, create_time, update_time = row
1459
+ return SessionRecord(
1460
+ id=session_id_val,
1461
+ app_name=app_name,
1462
+ user_id=user_id,
1463
+ state=self._deserialize_state(state_data_row),
1464
+ create_time=create_time,
1465
+ update_time=update_time,
1466
+ )
1467
+
1468
+ async def append_event_and_update_state(
1469
+ self, event_record: EventRecord, session_id: str, state: "dict[str, Any]"
1470
+ ) -> SessionRecord:
1471
+ """Atomically append an event and update the session's durable state."""
1472
+ return await async_(self._append_event_and_update_state)(event_record, session_id, state)
1473
+
1474
+ def _get_events(
1475
+ self, session_id: str, after_timestamp: "datetime | None" = None, limit: "int | None" = None
1476
+ ) -> "list[EventRecord]":
1477
+ """List events for a session ordered by timestamp.
1478
+
1479
+ Args:
1480
+ session_id: Session identifier.
1481
+ after_timestamp: Only return events after this time.
1482
+ limit: Maximum number of events to return.
1483
+
1484
+ Returns:
1485
+ List of event records ordered by timestamp ASC.
1486
+ """
1487
+
1488
+ where_clauses = ["session_id = :session_id"]
1489
+ params: dict[str, Any] = {"session_id": session_id}
1490
+
1491
+ if after_timestamp is not None:
1492
+ where_clauses.append("timestamp > :after_timestamp")
1493
+ params["after_timestamp"] = after_timestamp
1494
+
1495
+ where_clause = " AND ".join(where_clauses)
1496
+ limit_clause = f" FETCH FIRST {limit} ROWS ONLY" if limit else ""
1497
+ sql = f"""
1498
+ SELECT session_id, invocation_id, author, timestamp, event_json
1499
+ FROM {self._events_table}
1500
+ WHERE {where_clause}
1501
+ ORDER BY timestamp ASC{limit_clause}
1502
+ """
1503
+
1504
+ try:
1505
+ with self._config.provide_connection() as conn:
1506
+ cursor = conn.cursor()
1507
+ cursor.execute(sql, params)
1508
+ rows = cursor.fetchall()
1509
+
1510
+ return [
1511
+ EventRecord(
1512
+ session_id=row[0],
1513
+ invocation_id=_oracle_text_value(row[1]),
1514
+ author=_oracle_text_value(row[2]),
1515
+ timestamp=row[3],
1516
+ event_json=self._deserialize_json_field(row[4]) or {},
1517
+ )
1518
+ for row in rows
1519
+ ]
1520
+ except oracledb.DatabaseError as e:
1521
+ error_obj = e.args[0] if e.args else None
1522
+ if error_obj and error_obj.code == ORACLE_TABLE_NOT_FOUND_ERROR:
1523
+ return []
1524
+ raise
1525
+
1526
+ async def get_events(
1527
+ self, session_id: str, after_timestamp: "datetime | None" = None, limit: "int | None" = None
1528
+ ) -> "list[EventRecord]":
1529
+ """Get events for a session."""
1530
+ return await async_(self._get_events)(session_id, after_timestamp, limit)
1531
+
1532
+ def _append_event(self, event_record: EventRecord) -> None:
1533
+ """Synchronous implementation of append_event."""
1534
+ sql = f"""
1535
+ INSERT INTO {self._events_table} (
1536
+ session_id, invocation_id, author, timestamp, event_json
1537
+ ) VALUES (
1538
+ :session_id, :invocation_id, :author, :timestamp, :event_json
1539
+ )
1540
+ """
1541
+
1542
+ with self._config.provide_connection() as conn:
1543
+ cursor = conn.cursor()
1544
+ cursor.execute(
1545
+ sql,
1546
+ {
1547
+ "session_id": event_record["session_id"],
1548
+ "invocation_id": event_record["invocation_id"],
1549
+ "author": event_record["author"],
1550
+ "timestamp": event_record["timestamp"],
1551
+ "event_json": self._serialize_event_json(event_record["event_json"]),
1552
+ },
1553
+ )
1554
+ conn.commit()
1555
+
1556
+ async def append_event(self, event_record: EventRecord) -> None:
1557
+ """Append an event to a session."""
1558
+ await async_(self._append_event)(event_record)
1559
+
1560
+
1561
+ class OracleAsyncADKMemoryStore(BaseAsyncADKMemoryStore["OracleAsyncConfig"]):
1562
+ """Oracle ADK memory store using async oracledb driver."""
1563
+
1564
+ __slots__ = ("_in_memory", "_json_storage_type", "_oracle_version_info")
1565
+
1566
+ def __init__(self, config: "OracleAsyncConfig") -> None:
1567
+ super().__init__(config)
1568
+ self._json_storage_type: JSONStorageType | None = None
1569
+ self._oracle_version_info: OracleVersionInfo | None = None
1570
+ adk_config = config.extension_config.get("adk", {})
1571
+ self._in_memory: bool = bool(adk_config.get("in_memory", False))
1572
+
1573
+ async def _detect_json_storage_type(self) -> "JSONStorageType":
1574
+ if self._json_storage_type is not None:
1575
+ return self._json_storage_type
1576
+
1577
+ version_info = await self._get_version_info()
1578
+ self._json_storage_type = storage_type_from_version(version_info)
1579
+ return self._json_storage_type
1580
+
1581
+ async def _get_version_info(self) -> "OracleVersionInfo | None":
1582
+ if self._oracle_version_info is not None:
1583
+ return self._oracle_version_info
1584
+
1585
+ async with self._config.provide_session() as driver:
1586
+ dictionary = OracledbAsyncDataDictionary()
1587
+ self._oracle_version_info = await dictionary.get_version(driver)
1588
+
1589
+ if self._oracle_version_info is None:
1590
+ logger.warning("Could not detect Oracle version, defaulting to BLOB_JSON storage")
1591
+
1592
+ return self._oracle_version_info
1593
+
1594
+ async def _serialize_json_field(self, value: Any) -> "str | bytes | None":
1595
+ if value is None:
1596
+ return None
1597
+
1598
+ storage_type = await self._detect_json_storage_type()
1599
+ if storage_type == JSONStorageType.JSON_NATIVE:
1600
+ return to_json(value)
1601
+ return to_json(value, as_bytes=True)
1602
+
1603
+ async def _deserialize_json_field(self, data: Any) -> "dict[str, Any] | None":
1604
+ if data is None:
1605
+ return None
1606
+
1607
+ if is_async_readable(data) or is_readable(data):
1608
+ data = await _read_lob_async(data)
1609
+
1610
+ return _extract_json_value(data)
1611
+
1612
+ async def _get_create_memory_table_sql(self) -> str:
1613
+ storage_type = await self._detect_json_storage_type()
1614
+ return self._get_create_memory_table_sql_for_type(storage_type)
1615
+
1616
+ def _get_create_memory_table_sql_for_type(self, storage_type: "JSONStorageType") -> str:
1617
+ if storage_type == JSONStorageType.JSON_NATIVE:
1618
+ json_columns = """
1619
+ content_json JSON,
1620
+ metadata_json JSON
1621
+ """
1622
+ elif storage_type == JSONStorageType.BLOB_JSON:
1623
+ json_columns = """
1624
+ content_json BLOB CHECK (content_json IS JSON),
1625
+ metadata_json BLOB CHECK (metadata_json IS JSON)
1626
+ """
1627
+ else:
1628
+ json_columns = """
1629
+ content_json BLOB,
1630
+ metadata_json BLOB
1631
+ """
1632
+
1633
+ owner_id_line = f",\n {self._owner_id_column_ddl}" if self._owner_id_column_ddl else ""
1634
+ table_clauses = _oracle_table_feature_clauses(
1635
+ self._config,
1636
+ "memory",
1637
+ in_memory=self._in_memory,
1638
+ hash_partition_key="id",
1639
+ range_partition_key="inserted_at",
1640
+ )
1641
+
1642
+ fts_index = ""
1643
+ if self._use_fts:
1644
+ fts_index = f"""
1645
+ BEGIN
1646
+ EXECUTE IMMEDIATE 'CREATE INDEX idx_{self._memory_table}_fts
1647
+ ON {self._memory_table}(content_text) INDEXTYPE IS CTXSYS.CONTEXT';
1648
+ EXCEPTION
1649
+ WHEN OTHERS THEN
1650
+ IF SQLCODE != -955 THEN
1651
+ RAISE;
1652
+ END IF;
1653
+ END;
1654
+ """
1655
+
1656
+ return f"""
1657
+ BEGIN
1658
+ EXECUTE IMMEDIATE 'CREATE TABLE {self._memory_table} (
1659
+ id VARCHAR2(128) PRIMARY KEY,
1660
+ session_id VARCHAR2(128) NOT NULL,
1661
+ app_name VARCHAR2(128) NOT NULL,
1662
+ user_id VARCHAR2(128) NOT NULL,
1663
+ event_id VARCHAR2(128) NOT NULL UNIQUE,
1664
+ author VARCHAR2(256){owner_id_line},
1665
+ timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
1666
+ {json_columns},
1667
+ content_text CLOB NOT NULL,
1668
+ inserted_at TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL
1669
+ ){table_clauses}';
1670
+ EXCEPTION
1671
+ WHEN OTHERS THEN
1672
+ IF SQLCODE != -955 THEN
1673
+ RAISE;
1674
+ END IF;
1675
+ END;
1676
+
1677
+ BEGIN
1678
+ EXECUTE IMMEDIATE 'CREATE INDEX idx_{self._memory_table}_app_user_time
1679
+ ON {self._memory_table}(app_name, user_id, timestamp DESC)';
1680
+ EXCEPTION
1681
+ WHEN OTHERS THEN
1682
+ IF SQLCODE != -955 THEN
1683
+ RAISE;
1684
+ END IF;
1685
+ END;
1686
+
1687
+ BEGIN
1688
+ EXECUTE IMMEDIATE 'CREATE INDEX idx_{self._memory_table}_session
1689
+ ON {self._memory_table}(session_id)';
1690
+ EXCEPTION
1691
+ WHEN OTHERS THEN
1692
+ IF SQLCODE != -955 THEN
1693
+ RAISE;
1694
+ END IF;
1695
+ END;
1696
+ {fts_index}
1697
+ """
1698
+
1699
+ def _get_drop_memory_table_sql(self) -> "list[str]":
1700
+ return [
1701
+ f"""
1702
+ BEGIN
1703
+ EXECUTE IMMEDIATE 'DROP INDEX idx_{self._memory_table}_session';
1704
+ EXCEPTION
1705
+ WHEN OTHERS THEN
1706
+ IF SQLCODE != -1418 THEN
1707
+ RAISE;
1708
+ END IF;
1709
+ END;
1710
+ """,
1711
+ f"""
1712
+ BEGIN
1713
+ EXECUTE IMMEDIATE 'DROP INDEX idx_{self._memory_table}_app_user_time';
1714
+ EXCEPTION
1715
+ WHEN OTHERS THEN
1716
+ IF SQLCODE != -1418 THEN
1717
+ RAISE;
1718
+ END IF;
1719
+ END;
1720
+ """,
1721
+ f"""
1722
+ BEGIN
1723
+ EXECUTE IMMEDIATE 'DROP TABLE {self._memory_table}';
1724
+ EXCEPTION
1725
+ WHEN OTHERS THEN
1726
+ IF SQLCODE != -942 THEN
1727
+ RAISE;
1728
+ END IF;
1729
+ END;
1730
+ """,
1731
+ ]
1732
+
1733
+ async def create_tables(self) -> None:
1734
+ if not self._enabled:
1735
+ return
1736
+
1737
+ async with self._config.provide_session() as driver:
1738
+ await driver.execute_script(await self._get_create_memory_table_sql())
1739
+
1740
+ async def _execute_insert_entry(self, cursor: Any, sql: str, params: "dict[str, Any]") -> bool:
1741
+ """Execute an insert and skip duplicate key errors."""
1742
+ try:
1743
+ await cursor.execute(sql, params)
1744
+ except oracledb.DatabaseError as exc:
1745
+ error_obj = exc.args[0] if exc.args else None
1746
+ if error_obj and error_obj.code == ORACLE_DUPLICATE_KEY_ERROR:
1747
+ return False
1748
+ raise
1749
+ return True
1750
+
1751
+ async def insert_memory_entries(self, entries: "list[MemoryRecord]", owner_id: "object | None" = None) -> int:
1752
+ if not self._enabled:
1753
+ msg = "Memory store is disabled"
1754
+ raise RuntimeError(msg)
1755
+
1756
+ if not entries:
1757
+ return 0
1758
+
1759
+ owner_column = f", {self._owner_id_column_name}" if self._owner_id_column_name else ""
1760
+ owner_param = ", :owner_id" if self._owner_id_column_name else ""
1761
+ sql = f"""
1762
+ INSERT INTO {self._memory_table} (
1763
+ id, session_id, app_name, user_id, event_id, author{owner_column},
1764
+ timestamp, content_json, content_text, metadata_json, inserted_at
1765
+ ) VALUES (
1766
+ :id, :session_id, :app_name, :user_id, :event_id, :author{owner_param},
1767
+ :timestamp, :content_json, :content_text, :metadata_json, :inserted_at
1768
+ )
1769
+ """
1770
+
1771
+ inserted_count = 0
1772
+ async with self._config.provide_connection() as conn:
1773
+ cursor = conn.cursor()
1774
+ for entry in entries:
1775
+ content_json = await self._serialize_json_field(entry["content_json"])
1776
+ metadata_json = await self._serialize_json_field(entry["metadata_json"])
1777
+ params = {
1778
+ "id": entry["id"],
1779
+ "session_id": entry["session_id"],
1780
+ "app_name": entry["app_name"],
1781
+ "user_id": entry["user_id"],
1782
+ "event_id": entry["event_id"],
1783
+ "author": entry["author"],
1784
+ "timestamp": entry["timestamp"],
1785
+ "content_json": content_json,
1786
+ "content_text": entry["content_text"],
1787
+ "metadata_json": metadata_json,
1788
+ "inserted_at": entry["inserted_at"],
1789
+ }
1790
+ if self._owner_id_column_name:
1791
+ params["owner_id"] = str(owner_id) if owner_id is not None else None
1792
+ if await self._execute_insert_entry(cursor, sql, params):
1793
+ inserted_count += 1
1794
+ await conn.commit()
1795
+
1796
+ return inserted_count
1797
+
1798
+ async def search_entries(
1799
+ self, query: str, app_name: str, user_id: str, limit: "int | None" = None
1800
+ ) -> "list[MemoryRecord]":
1801
+ if not self._enabled:
1802
+ msg = "Memory store is disabled"
1803
+ raise RuntimeError(msg)
1804
+
1805
+ effective_limit = limit if limit is not None else self._max_results
1806
+
1807
+ try:
1808
+ if self._use_fts:
1809
+ return await self._search_entries_fts(query, app_name, user_id, effective_limit)
1810
+ return await self._search_entries_simple(query, app_name, user_id, effective_limit)
1811
+ except oracledb.DatabaseError as exc:
1812
+ error_obj = exc.args[0] if exc.args else None
1813
+ if error_obj and error_obj.code == ORACLE_TABLE_NOT_FOUND_ERROR:
1814
+ return []
1815
+ raise
1816
+
1817
+ async def _search_entries_fts(self, query: str, app_name: str, user_id: str, limit: int) -> "list[MemoryRecord]":
1818
+ sql = f"""
1819
+ SELECT id, session_id, app_name, user_id, event_id, author,
1820
+ timestamp, content_json, content_text, metadata_json, inserted_at
1821
+ FROM (
1822
+ SELECT id, session_id, app_name, user_id, event_id, author,
1823
+ timestamp, content_json, content_text, metadata_json, inserted_at,
1824
+ SCORE(1) AS score
1825
+ FROM {self._memory_table}
1826
+ WHERE app_name = :app_name
1827
+ AND user_id = :user_id
1828
+ AND CONTAINS(content_text, :query, 1) > 0
1829
+ ORDER BY score DESC, timestamp DESC
1830
+ )
1831
+ WHERE ROWNUM <= :limit
1832
+ """
1833
+ params = {"app_name": app_name, "user_id": user_id, "query": query, "limit": limit}
1834
+ async with self._config.provide_connection() as conn:
1835
+ cursor = conn.cursor()
1836
+ await cursor.execute(sql, params)
1837
+ rows = await cursor.fetchall()
1838
+ return await self._rows_to_records(rows)
1839
+
1840
+ async def _search_entries_simple(self, query: str, app_name: str, user_id: str, limit: int) -> "list[MemoryRecord]":
1841
+ sql = f"""
1842
+ SELECT id, session_id, app_name, user_id, event_id, author,
1843
+ timestamp, content_json, content_text, metadata_json, inserted_at
1844
+ FROM (
1845
+ SELECT id, session_id, app_name, user_id, event_id, author,
1846
+ timestamp, content_json, content_text, metadata_json, inserted_at
1847
+ FROM {self._memory_table}
1848
+ WHERE app_name = :app_name
1849
+ AND user_id = :user_id
1850
+ AND LOWER(content_text) LIKE :pattern
1851
+ ORDER BY timestamp DESC
1852
+ )
1853
+ WHERE ROWNUM <= :limit
1854
+ """
1855
+ pattern = f"%{query.lower()}%"
1856
+ params = {"app_name": app_name, "user_id": user_id, "pattern": pattern, "limit": limit}
1857
+ async with self._config.provide_connection() as conn:
1858
+ cursor = conn.cursor()
1859
+ await cursor.execute(sql, params)
1860
+ rows = await cursor.fetchall()
1861
+ return await self._rows_to_records(rows)
1862
+
1863
+ async def delete_entries_by_session(self, session_id: str) -> int:
1864
+ sql = f"DELETE FROM {self._memory_table} WHERE session_id = :session_id"
1865
+ async with self._config.provide_connection() as conn:
1866
+ cursor = conn.cursor()
1867
+ await cursor.execute(sql, {"session_id": session_id})
1868
+ await conn.commit()
1869
+ return cursor.rowcount if cursor.rowcount and cursor.rowcount > 0 else 0
1870
+
1871
+ async def delete_entries_older_than(self, days: int) -> int:
1872
+ sql = f"""
1873
+ DELETE FROM {self._memory_table}
1874
+ WHERE inserted_at < SYSTIMESTAMP - NUMTODSINTERVAL(:days, 'DAY')
1875
+ """
1876
+ async with self._config.provide_connection() as conn:
1877
+ cursor = conn.cursor()
1878
+ await cursor.execute(sql, {"days": days})
1879
+ await conn.commit()
1880
+ return cursor.rowcount if cursor.rowcount and cursor.rowcount > 0 else 0
1881
+
1882
+ async def _rows_to_records(self, rows: "list[Any]") -> "list[MemoryRecord]":
1883
+ records: list[MemoryRecord] = []
1884
+ for row in rows:
1885
+ content_json = await self._deserialize_json_field(row[7]) if row[7] is not None else {}
1886
+ metadata_json = await self._deserialize_json_field(row[9])
1887
+ content_text = row[8]
1888
+ if is_async_readable(content_text) or is_readable(content_text):
1889
+ content_text = await _read_lob_async(content_text)
1890
+ records.append({
1891
+ "id": row[0],
1892
+ "session_id": row[1],
1893
+ "app_name": row[2],
1894
+ "user_id": row[3],
1895
+ "event_id": row[4],
1896
+ "author": row[5],
1897
+ "timestamp": row[6],
1898
+ "content_json": cast("dict[str, Any]", content_json),
1899
+ "content_text": str(content_text),
1900
+ "metadata_json": metadata_json,
1901
+ "inserted_at": row[10],
1902
+ })
1903
+ return records
1904
+
1905
+
1906
+ class OracleSyncADKMemoryStore(BaseAsyncADKMemoryStore["OracleSyncConfig"]):
1907
+ """Oracle ADK memory store using sync oracledb driver."""
1908
+
1909
+ __slots__ = ("_in_memory", "_json_storage_type", "_oracle_version_info")
1910
+
1911
+ def __init__(self, config: "OracleSyncConfig") -> None:
1912
+ super().__init__(config)
1913
+ self._json_storage_type: JSONStorageType | None = None
1914
+ self._oracle_version_info: OracleVersionInfo | None = None
1915
+ adk_config = config.extension_config.get("adk", {})
1916
+ self._in_memory = bool(adk_config.get("in_memory", False))
1917
+
1918
+ def _detect_json_storage_type(self) -> "JSONStorageType":
1919
+ if self._json_storage_type is not None:
1920
+ return self._json_storage_type
1921
+
1922
+ version_info = self._get_version_info()
1923
+ self._json_storage_type = storage_type_from_version(version_info)
1924
+ return self._json_storage_type
1925
+
1926
+ def _get_version_info(self) -> "OracleVersionInfo | None":
1927
+ if self._oracle_version_info is not None:
1928
+ return self._oracle_version_info
1929
+
1930
+ with self._config.provide_session() as driver:
1931
+ dictionary = OracledbSyncDataDictionary()
1932
+ self._oracle_version_info = dictionary.get_version(driver)
1933
+
1934
+ if self._oracle_version_info is None:
1935
+ logger.warning("Could not detect Oracle version, defaulting to BLOB_JSON storage")
1936
+
1937
+ return self._oracle_version_info
1938
+
1939
+ def _serialize_json_field(self, value: Any) -> "str | bytes | None":
1940
+ if value is None:
1941
+ return None
1942
+
1943
+ storage_type = self._detect_json_storage_type()
1944
+ if storage_type == JSONStorageType.JSON_NATIVE:
1945
+ return to_json(value)
1946
+ return to_json(value, as_bytes=True)
1947
+
1948
+ def _deserialize_json_field(self, data: Any) -> "dict[str, Any] | None":
1949
+ if data is None:
1950
+ return None
1951
+
1952
+ if is_readable(data):
1953
+ data = _read_lob_sync(data)
1954
+
1955
+ return _extract_json_value(data)
1956
+
1957
+ async def _get_create_memory_table_sql(self) -> str:
1958
+ storage_type = self._detect_json_storage_type()
1959
+ return self._get_create_memory_table_sql_for_type(storage_type)
1960
+
1961
+ def _get_create_memory_table_sql_for_type(self, storage_type: "JSONStorageType") -> str:
1962
+ if storage_type == JSONStorageType.JSON_NATIVE:
1963
+ json_columns = """
1964
+ content_json JSON,
1965
+ metadata_json JSON
1966
+ """
1967
+ elif storage_type == JSONStorageType.BLOB_JSON:
1968
+ json_columns = """
1969
+ content_json BLOB CHECK (content_json IS JSON),
1970
+ metadata_json BLOB CHECK (metadata_json IS JSON)
1971
+ """
1972
+ else:
1973
+ json_columns = """
1974
+ content_json BLOB,
1975
+ metadata_json BLOB
1976
+ """
1977
+
1978
+ owner_id_line = f",\n {self._owner_id_column_ddl}" if self._owner_id_column_ddl else ""
1979
+ table_clauses = _oracle_table_feature_clauses(
1980
+ self._config,
1981
+ "memory",
1982
+ in_memory=self._in_memory,
1983
+ hash_partition_key="id",
1984
+ range_partition_key="inserted_at",
1985
+ )
1986
+
1987
+ fts_index = ""
1988
+ if self._use_fts:
1989
+ fts_index = f"""
1990
+ BEGIN
1991
+ EXECUTE IMMEDIATE 'CREATE INDEX idx_{self._memory_table}_fts
1992
+ ON {self._memory_table}(content_text) INDEXTYPE IS CTXSYS.CONTEXT';
1993
+ EXCEPTION
1994
+ WHEN OTHERS THEN
1995
+ IF SQLCODE != -955 THEN
1996
+ RAISE;
1997
+ END IF;
1998
+ END;
1999
+ """
2000
+
2001
+ return f"""
2002
+ BEGIN
2003
+ EXECUTE IMMEDIATE 'CREATE TABLE {self._memory_table} (
2004
+ id VARCHAR2(128) PRIMARY KEY,
2005
+ session_id VARCHAR2(128) NOT NULL,
2006
+ app_name VARCHAR2(128) NOT NULL,
2007
+ user_id VARCHAR2(128) NOT NULL,
2008
+ event_id VARCHAR2(128) NOT NULL UNIQUE,
2009
+ author VARCHAR2(256){owner_id_line},
2010
+ timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
2011
+ {json_columns},
2012
+ content_text CLOB NOT NULL,
2013
+ inserted_at TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL
2014
+ ){table_clauses}';
2015
+ EXCEPTION
2016
+ WHEN OTHERS THEN
2017
+ IF SQLCODE != -955 THEN
2018
+ RAISE;
2019
+ END IF;
2020
+ END;
2021
+
2022
+ BEGIN
2023
+ EXECUTE IMMEDIATE 'CREATE INDEX idx_{self._memory_table}_app_user_time
2024
+ ON {self._memory_table}(app_name, user_id, timestamp DESC)';
2025
+ EXCEPTION
2026
+ WHEN OTHERS THEN
2027
+ IF SQLCODE != -955 THEN
2028
+ RAISE;
2029
+ END IF;
2030
+ END;
2031
+
2032
+ BEGIN
2033
+ EXECUTE IMMEDIATE 'CREATE INDEX idx_{self._memory_table}_session
2034
+ ON {self._memory_table}(session_id)';
2035
+ EXCEPTION
2036
+ WHEN OTHERS THEN
2037
+ IF SQLCODE != -955 THEN
2038
+ RAISE;
2039
+ END IF;
2040
+ END;
2041
+ {fts_index}
2042
+ """
2043
+
2044
+ def _get_drop_memory_table_sql(self) -> "list[str]":
2045
+ return [
2046
+ f"""
2047
+ BEGIN
2048
+ EXECUTE IMMEDIATE 'DROP INDEX idx_{self._memory_table}_session';
2049
+ EXCEPTION
2050
+ WHEN OTHERS THEN
2051
+ IF SQLCODE != -1418 THEN
2052
+ RAISE;
2053
+ END IF;
2054
+ END;
2055
+ """,
2056
+ f"""
2057
+ BEGIN
2058
+ EXECUTE IMMEDIATE 'DROP INDEX idx_{self._memory_table}_app_user_time';
2059
+ EXCEPTION
2060
+ WHEN OTHERS THEN
2061
+ IF SQLCODE != -1418 THEN
2062
+ RAISE;
2063
+ END IF;
2064
+ END;
2065
+ """,
2066
+ f"""
2067
+ BEGIN
2068
+ EXECUTE IMMEDIATE 'DROP TABLE {self._memory_table}';
2069
+ EXCEPTION
2070
+ WHEN OTHERS THEN
2071
+ IF SQLCODE != -942 THEN
2072
+ RAISE;
2073
+ END IF;
2074
+ END;
2075
+ """,
2076
+ ]
2077
+
2078
+ def _create_tables(self) -> None:
2079
+ if not self._enabled:
2080
+ return
2081
+
2082
+ with self._config.provide_session() as driver:
2083
+ driver.execute_script(run_(self._get_create_memory_table_sql)())
2084
+
2085
+ async def create_tables(self) -> None:
2086
+ """Create tables if they don't exist."""
2087
+ await async_(self._create_tables)()
2088
+
2089
+ def _execute_insert_entry(self, cursor: Any, sql: str, params: "dict[str, Any]") -> bool:
2090
+ """Execute an insert and skip duplicate key errors."""
2091
+ try:
2092
+ cursor.execute(sql, params)
2093
+ except oracledb.DatabaseError as exc:
2094
+ error_obj = exc.args[0] if exc.args else None
2095
+ if error_obj and error_obj.code == ORACLE_DUPLICATE_KEY_ERROR:
2096
+ return False
2097
+ raise
2098
+ return True
2099
+
2100
+ def _insert_memory_entries(self, entries: "list[MemoryRecord]", owner_id: "object | None" = None) -> int:
2101
+ if not self._enabled:
2102
+ msg = "Memory store is disabled"
2103
+ raise RuntimeError(msg)
2104
+
2105
+ if not entries:
2106
+ return 0
2107
+
2108
+ owner_column = f", {self._owner_id_column_name}" if self._owner_id_column_name else ""
2109
+ owner_param = ", :owner_id" if self._owner_id_column_name else ""
2110
+ sql = f"""
2111
+ INSERT INTO {self._memory_table} (
2112
+ id, session_id, app_name, user_id, event_id, author{owner_column},
2113
+ timestamp, content_json, content_text, metadata_json, inserted_at
2114
+ ) VALUES (
2115
+ :id, :session_id, :app_name, :user_id, :event_id, :author{owner_param},
2116
+ :timestamp, :content_json, :content_text, :metadata_json, :inserted_at
2117
+ )
2118
+ """
2119
+
2120
+ inserted_count = 0
2121
+ with self._config.provide_connection() as conn:
2122
+ cursor = conn.cursor()
2123
+ for entry in entries:
2124
+ content_json = self._serialize_json_field(entry["content_json"])
2125
+ metadata_json = self._serialize_json_field(entry["metadata_json"])
2126
+ params = {
2127
+ "id": entry["id"],
2128
+ "session_id": entry["session_id"],
2129
+ "app_name": entry["app_name"],
2130
+ "user_id": entry["user_id"],
2131
+ "event_id": entry["event_id"],
2132
+ "author": entry["author"],
2133
+ "timestamp": entry["timestamp"],
2134
+ "content_json": content_json,
2135
+ "content_text": entry["content_text"],
2136
+ "metadata_json": metadata_json,
2137
+ "inserted_at": entry["inserted_at"],
2138
+ }
2139
+ if self._owner_id_column_name:
2140
+ params["owner_id"] = str(owner_id) if owner_id is not None else None
2141
+ if self._execute_insert_entry(cursor, sql, params):
2142
+ inserted_count += 1
2143
+ conn.commit()
2144
+
2145
+ return inserted_count
2146
+
2147
+ async def insert_memory_entries(self, entries: "list[MemoryRecord]", owner_id: "object | None" = None) -> int:
2148
+ """Bulk insert memory entries with deduplication."""
2149
+ return await async_(self._insert_memory_entries)(entries, owner_id)
2150
+
2151
+ def _search_entries(
2152
+ self, query: str, app_name: str, user_id: str, limit: "int | None" = None
2153
+ ) -> "list[MemoryRecord]":
2154
+ if not self._enabled:
2155
+ msg = "Memory store is disabled"
2156
+ raise RuntimeError(msg)
2157
+
2158
+ effective_limit = limit if limit is not None else self._max_results
2159
+
2160
+ try:
2161
+ if self._use_fts:
2162
+ return self._search_entries_fts(query, app_name, user_id, effective_limit)
2163
+ return self._search_entries_simple(query, app_name, user_id, effective_limit)
2164
+ except oracledb.DatabaseError as exc:
2165
+ error_obj = exc.args[0] if exc.args else None
2166
+ if error_obj and error_obj.code == ORACLE_TABLE_NOT_FOUND_ERROR:
2167
+ return []
2168
+ raise
2169
+
2170
+ async def search_entries(
2171
+ self, query: str, app_name: str, user_id: str, limit: "int | None" = None
2172
+ ) -> "list[MemoryRecord]":
2173
+ """Search memory entries by text query."""
2174
+ return await async_(self._search_entries)(query, app_name, user_id, limit)
2175
+
2176
+ def _search_entries_fts(self, query: str, app_name: str, user_id: str, limit: int) -> "list[MemoryRecord]":
2177
+ sql = f"""
2178
+ SELECT id, session_id, app_name, user_id, event_id, author,
2179
+ timestamp, content_json, content_text, metadata_json, inserted_at
2180
+ FROM (
2181
+ SELECT id, session_id, app_name, user_id, event_id, author,
2182
+ timestamp, content_json, content_text, metadata_json, inserted_at,
2183
+ SCORE(1) AS score
2184
+ FROM {self._memory_table}
2185
+ WHERE app_name = :app_name
2186
+ AND user_id = :user_id
2187
+ AND CONTAINS(content_text, :query, 1) > 0
2188
+ ORDER BY score DESC, timestamp DESC
2189
+ )
2190
+ WHERE ROWNUM <= :limit
2191
+ """
2192
+ params = {"app_name": app_name, "user_id": user_id, "query": query, "limit": limit}
2193
+ with self._config.provide_connection() as conn:
2194
+ cursor = conn.cursor()
2195
+ cursor.execute(sql, params)
2196
+ rows = cursor.fetchall()
2197
+ return self._rows_to_records(rows)
2198
+
2199
+ def _search_entries_simple(self, query: str, app_name: str, user_id: str, limit: int) -> "list[MemoryRecord]":
2200
+ sql = f"""
2201
+ SELECT id, session_id, app_name, user_id, event_id, author,
2202
+ timestamp, content_json, content_text, metadata_json, inserted_at
2203
+ FROM (
2204
+ SELECT id, session_id, app_name, user_id, event_id, author,
2205
+ timestamp, content_json, content_text, metadata_json, inserted_at
2206
+ FROM {self._memory_table}
2207
+ WHERE app_name = :app_name
2208
+ AND user_id = :user_id
2209
+ AND LOWER(content_text) LIKE :pattern
2210
+ ORDER BY timestamp DESC
2211
+ )
2212
+ WHERE ROWNUM <= :limit
2213
+ """
2214
+ pattern = f"%{query.lower()}%"
2215
+ params = {"app_name": app_name, "user_id": user_id, "pattern": pattern, "limit": limit}
2216
+ with self._config.provide_connection() as conn:
2217
+ cursor = conn.cursor()
2218
+ cursor.execute(sql, params)
2219
+ rows = cursor.fetchall()
2220
+ return self._rows_to_records(rows)
2221
+
2222
+ def _delete_entries_by_session(self, session_id: str) -> int:
2223
+ sql = f"DELETE FROM {self._memory_table} WHERE session_id = :session_id"
2224
+ with self._config.provide_connection() as conn:
2225
+ cursor = conn.cursor()
2226
+ cursor.execute(sql, {"session_id": session_id})
2227
+ conn.commit()
2228
+ return cursor.rowcount if cursor.rowcount and cursor.rowcount > 0 else 0
2229
+
2230
+ async def delete_entries_by_session(self, session_id: str) -> int:
2231
+ """Delete all memory entries for a specific session."""
2232
+ return await async_(self._delete_entries_by_session)(session_id)
2233
+
2234
+ def _delete_entries_older_than(self, days: int) -> int:
2235
+ sql = f"""
2236
+ DELETE FROM {self._memory_table}
2237
+ WHERE inserted_at < SYSTIMESTAMP - NUMTODSINTERVAL(:days, 'DAY')
2238
+ """
2239
+ with self._config.provide_connection() as conn:
2240
+ cursor = conn.cursor()
2241
+ cursor.execute(sql, {"days": days})
2242
+ conn.commit()
2243
+ return cursor.rowcount if cursor.rowcount and cursor.rowcount > 0 else 0
2244
+
2245
+ async def delete_entries_older_than(self, days: int) -> int:
2246
+ """Delete memory entries older than specified days."""
2247
+ return await async_(self._delete_entries_older_than)(days)
2248
+
2249
+ def _rows_to_records(self, rows: "list[Any]") -> "list[MemoryRecord]":
2250
+ records: list[MemoryRecord] = []
2251
+ for row in rows:
2252
+ content_json = self._deserialize_json_field(row[7]) if row[7] is not None else {}
2253
+ metadata_json = self._deserialize_json_field(row[9])
2254
+ content_text = row[8]
2255
+ if is_readable(content_text):
2256
+ content_text = _read_lob_sync(content_text)
2257
+ records.append({
2258
+ "id": row[0],
2259
+ "session_id": row[1],
2260
+ "app_name": row[2],
2261
+ "user_id": row[3],
2262
+ "event_id": row[4],
2263
+ "author": row[5],
2264
+ "timestamp": row[6],
2265
+ "content_json": cast("dict[str, Any]", content_json),
2266
+ "content_text": str(content_text),
2267
+ "metadata_json": metadata_json,
2268
+ "inserted_at": row[10],
2269
+ })
2270
+ return records
2271
+
2272
+
2273
+ def _coerce_decimal_values(value: Any) -> Any:
2274
+ if isinstance(value, Decimal):
2275
+ return float(value)
2276
+ if isinstance(value, dict):
2277
+ return {key: _coerce_decimal_values(val) for key, val in value.items()}
2278
+ if isinstance(value, list):
2279
+ return [_coerce_decimal_values(item) for item in value]
2280
+ if isinstance(value, tuple):
2281
+ return tuple(_coerce_decimal_values(item) for item in value)
2282
+ if isinstance(value, set):
2283
+ return {_coerce_decimal_values(item) for item in value}
2284
+ if isinstance(value, frozenset):
2285
+ return frozenset(_coerce_decimal_values(item) for item in value)
2286
+ return value
2287
+
2288
+
2289
+ def _storage_type_from_version(version_info: "OracleVersionInfo | None") -> JSONStorageType:
2290
+ """Determine JSON storage type based on Oracle version metadata."""
2291
+
2292
+ if version_info and version_info.supports_native_json():
2293
+ logger.debug("Detected Oracle %s with compatible >= 20, using JSON_NATIVE", version_info)
2294
+ return JSONStorageType.JSON_NATIVE
2295
+
2296
+ if version_info and version_info.supports_json_blob():
2297
+ logger.debug("Detected Oracle %s, using BLOB_JSON (recommended)", version_info)
2298
+ return JSONStorageType.BLOB_JSON
2299
+
2300
+ if version_info:
2301
+ logger.debug("Detected Oracle %s (pre-12c), using BLOB_PLAIN", version_info)
2302
+ return JSONStorageType.BLOB_PLAIN
2303
+
2304
+ logger.warning("Oracle version could not be detected; defaulting to BLOB_JSON storage")
2305
+ return JSONStorageType.BLOB_JSON
2306
+
2307
+
2308
+ def _oracle_text_value(value: Any) -> str:
2309
+ """Normalize Oracle VARCHAR2 values back to Python strings.
2310
+
2311
+ Oracle stores empty strings as ``NULL``. The ADK event contract allows
2312
+ empty strings for fields like ``invocation_id``, so reads coerce ``NULL``
2313
+ back to ``""``.
2314
+ """
2315
+ return "" if value is None else str(value)
2316
+
2317
+
2318
+ def _extract_json_value(data: Any) -> "dict[str, Any]":
2319
+ if isinstance(data, dict):
2320
+ return cast("dict[str, Any]", coerce_decimal_values(data))
2321
+ if isinstance(data, bytes):
2322
+ return from_json(data) # type: ignore[no-any-return]
2323
+ if isinstance(data, str):
2324
+ return from_json(data) # type: ignore[no-any-return]
2325
+ return from_json(str(data)) # type: ignore[no-any-return]
2326
+
2327
+
2328
+ def _event_json_column_ddl(storage_type: JSONStorageType) -> str:
2329
+ """Return the DDL fragment for the event_json column."""
2330
+ if storage_type == JSONStorageType.JSON_NATIVE:
2331
+ return "event_json JSON NOT NULL"
2332
+ if storage_type == JSONStorageType.BLOB_JSON:
2333
+ return "event_json BLOB CHECK (event_json IS JSON) NOT NULL"
2334
+ return "event_json BLOB NOT NULL"
2335
+
2336
+
2337
+ def _get_oracle_adk_config(config: Any) -> dict[str, Any]:
2338
+ adk_config = config.extension_config.get("adk", {})
2339
+ if isinstance(adk_config, dict):
2340
+ return adk_config
2341
+ return {}
2342
+
2343
+
2344
+ def _validate_oracle_identifier(value: str, label: str) -> str:
2345
+ if not value or not (value[0].isalpha() or value[0] == "_"):
2346
+ msg = f"Invalid Oracle {label}: {value!r}"
2347
+ raise ValueError(msg)
2348
+ if any(not (char.isalnum() or char == "_") for char in value):
2349
+ msg = f"Invalid Oracle {label}: {value!r}"
2350
+ raise ValueError(msg)
2351
+ return value
2352
+
2353
+
2354
+ def _oracle_compression_clause(adk_config: dict[str, Any]) -> str:
2355
+ compression = adk_config.get("compression")
2356
+ if not isinstance(compression, dict) or not compression.get("enabled"):
2357
+ return ""
2358
+
2359
+ algorithm = str(compression.get("algorithm") or "advanced").lower()
2360
+ try:
2361
+ return ORACLE_COMPRESSION_CLAUSES[algorithm]
2362
+ except KeyError as exc:
2363
+ supported = ", ".join(sorted(ORACLE_COMPRESSION_CLAUSES))
2364
+ msg = f"Unsupported Oracle ADK compression algorithm {algorithm!r}. Supported values: {supported}"
2365
+ raise ValueError(msg) from exc
2366
+
2367
+
2368
+ def _oracle_hash_partition_clause(partitioning: dict[str, Any], partition_key: str) -> str:
2369
+ partition_count = partitioning.get(
2370
+ "partition_count", partitioning.get("partitions", ORACLE_DEFAULT_HASH_PARTITIONS)
2371
+ )
2372
+ if not isinstance(partition_count, int) or partition_count < ORACLE_MIN_HASH_PARTITIONS:
2373
+ msg = "Oracle ADK hash partitioning requires partition_count >= 2"
2374
+ raise ValueError(msg)
2375
+ return f"PARTITION BY HASH ({partition_key}) PARTITIONS {partition_count}"
2376
+
2377
+
2378
+ def _oracle_range_partition_clause(partitioning: dict[str, Any], partition_key: str) -> str:
2379
+ interval = str(partitioning.get("interval") or "month").lower()
2380
+ interval_sql = ORACLE_RANGE_INTERVALS.get(interval)
2381
+ if interval_sql is None:
2382
+ supported = ", ".join(sorted(ORACLE_RANGE_INTERVALS))
2383
+ msg = f"Unsupported Oracle ADK range partition interval {interval!r}. Supported values: {supported}"
2384
+ raise ValueError(msg)
2385
+
2386
+ initial_less_than = str(partitioning.get("initial_less_than") or "TIMESTAMP '2000-01-01 00:00:00'")
2387
+ return (
2388
+ f"PARTITION BY RANGE ({partition_key}) INTERVAL ({interval_sql}) "
2389
+ f"(PARTITION p_initial VALUES LESS THAN ({initial_less_than}))"
2390
+ )
2391
+
2392
+
2393
+ def _oracle_partition_clause(
2394
+ adk_config: dict[str, Any], table_kind: str, hash_partition_key: str, range_partition_key: str
2395
+ ) -> str:
2396
+ partitioning = adk_config.get("partitioning")
2397
+ if not isinstance(partitioning, dict):
2398
+ return ""
2399
+
2400
+ strategy = str(partitioning.get("strategy") or "").lower()
2401
+ if not strategy:
2402
+ return ""
2403
+
2404
+ table_key = partitioning.get(f"{table_kind}_partition_key")
2405
+ configured_key = table_key if table_key is not None else partitioning.get("partition_key")
2406
+ if strategy == "hash":
2407
+ partition_key = _validate_oracle_identifier(str(configured_key or hash_partition_key), "partition key")
2408
+ return _oracle_hash_partition_clause(partitioning, partition_key)
2409
+ if strategy == "range":
2410
+ partition_key = _validate_oracle_identifier(str(configured_key or range_partition_key), "partition key")
2411
+ return _oracle_range_partition_clause(partitioning, partition_key)
2412
+
2413
+ msg = f"Unsupported Oracle ADK partitioning strategy {strategy!r}. Supported values: hash, range"
2414
+ raise ValueError(msg)
2415
+
2416
+
2417
+ def _oracle_table_options_clause(adk_config: dict[str, Any], table_kind: str) -> str:
2418
+ option_key = "events_table_options" if table_kind == "events" else f"{table_kind}_table_options"
2419
+ options = adk_config.get(option_key)
2420
+ return str(options).strip() if options else ""
2421
+
2422
+
2423
+ def _oracle_table_feature_clauses(
2424
+ config: Any, table_kind: str, *, in_memory: bool, hash_partition_key: str, range_partition_key: str
2425
+ ) -> str:
2426
+ adk_config = _get_oracle_adk_config(config)
2427
+ clauses = [
2428
+ clause
2429
+ for clause in (
2430
+ _oracle_compression_clause(adk_config),
2431
+ "INMEMORY PRIORITY HIGH" if in_memory else "",
2432
+ _oracle_table_options_clause(adk_config, table_kind),
2433
+ _oracle_partition_clause(adk_config, table_kind, hash_partition_key, range_partition_key),
2434
+ )
2435
+ if clause
2436
+ ]
2437
+ if not clauses:
2438
+ return ""
2439
+ return " " + " ".join(clauses).replace("'", "''")
2440
+
2441
+
2442
+ async def _read_lob_async(data: Any) -> Any:
2443
+ if is_async_readable(data):
2444
+ return await data.read()
2445
+ if is_readable(data):
2446
+ return data.read()
2447
+ return data
2448
+
2449
+
2450
+ def _read_lob_sync(data: Any) -> Any:
2451
+ if is_readable(data):
2452
+ return data.read()
2453
+ return data