sqlspec 0.36.0__cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (531) hide show
  1. ac8f31065839703b4e70__mypyc.cpython-310-aarch64-linux-gnu.so +0 -0
  2. sqlspec/__init__.py +140 -0
  3. sqlspec/__main__.py +12 -0
  4. sqlspec/__metadata__.py +14 -0
  5. sqlspec/_serialization.py +315 -0
  6. sqlspec/_typing.py +700 -0
  7. sqlspec/adapters/__init__.py +0 -0
  8. sqlspec/adapters/adbc/__init__.py +5 -0
  9. sqlspec/adapters/adbc/_typing.py +82 -0
  10. sqlspec/adapters/adbc/adk/__init__.py +5 -0
  11. sqlspec/adapters/adbc/adk/store.py +1273 -0
  12. sqlspec/adapters/adbc/config.py +295 -0
  13. sqlspec/adapters/adbc/core.cpython-310-aarch64-linux-gnu.so +0 -0
  14. sqlspec/adapters/adbc/core.py +735 -0
  15. sqlspec/adapters/adbc/data_dictionary.py +334 -0
  16. sqlspec/adapters/adbc/driver.py +529 -0
  17. sqlspec/adapters/adbc/events/__init__.py +5 -0
  18. sqlspec/adapters/adbc/events/store.py +285 -0
  19. sqlspec/adapters/adbc/litestar/__init__.py +5 -0
  20. sqlspec/adapters/adbc/litestar/store.py +502 -0
  21. sqlspec/adapters/adbc/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
  22. sqlspec/adapters/adbc/type_converter.py +140 -0
  23. sqlspec/adapters/aiosqlite/__init__.py +25 -0
  24. sqlspec/adapters/aiosqlite/_typing.py +82 -0
  25. sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
  26. sqlspec/adapters/aiosqlite/adk/store.py +818 -0
  27. sqlspec/adapters/aiosqlite/config.py +334 -0
  28. sqlspec/adapters/aiosqlite/core.cpython-310-aarch64-linux-gnu.so +0 -0
  29. sqlspec/adapters/aiosqlite/core.py +315 -0
  30. sqlspec/adapters/aiosqlite/data_dictionary.py +208 -0
  31. sqlspec/adapters/aiosqlite/driver.py +313 -0
  32. sqlspec/adapters/aiosqlite/events/__init__.py +5 -0
  33. sqlspec/adapters/aiosqlite/events/store.py +20 -0
  34. sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
  35. sqlspec/adapters/aiosqlite/litestar/store.py +279 -0
  36. sqlspec/adapters/aiosqlite/pool.py +533 -0
  37. sqlspec/adapters/asyncmy/__init__.py +21 -0
  38. sqlspec/adapters/asyncmy/_typing.py +87 -0
  39. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  40. sqlspec/adapters/asyncmy/adk/store.py +703 -0
  41. sqlspec/adapters/asyncmy/config.py +302 -0
  42. sqlspec/adapters/asyncmy/core.cpython-310-aarch64-linux-gnu.so +0 -0
  43. sqlspec/adapters/asyncmy/core.py +360 -0
  44. sqlspec/adapters/asyncmy/data_dictionary.py +124 -0
  45. sqlspec/adapters/asyncmy/driver.py +383 -0
  46. sqlspec/adapters/asyncmy/events/__init__.py +5 -0
  47. sqlspec/adapters/asyncmy/events/store.py +104 -0
  48. sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
  49. sqlspec/adapters/asyncmy/litestar/store.py +296 -0
  50. sqlspec/adapters/asyncpg/__init__.py +19 -0
  51. sqlspec/adapters/asyncpg/_typing.py +88 -0
  52. sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
  53. sqlspec/adapters/asyncpg/adk/store.py +748 -0
  54. sqlspec/adapters/asyncpg/config.py +569 -0
  55. sqlspec/adapters/asyncpg/core.cpython-310-aarch64-linux-gnu.so +0 -0
  56. sqlspec/adapters/asyncpg/core.py +367 -0
  57. sqlspec/adapters/asyncpg/data_dictionary.py +162 -0
  58. sqlspec/adapters/asyncpg/driver.py +487 -0
  59. sqlspec/adapters/asyncpg/events/__init__.py +6 -0
  60. sqlspec/adapters/asyncpg/events/backend.py +286 -0
  61. sqlspec/adapters/asyncpg/events/store.py +40 -0
  62. sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
  63. sqlspec/adapters/asyncpg/litestar/store.py +251 -0
  64. sqlspec/adapters/bigquery/__init__.py +14 -0
  65. sqlspec/adapters/bigquery/_typing.py +86 -0
  66. sqlspec/adapters/bigquery/adk/__init__.py +5 -0
  67. sqlspec/adapters/bigquery/adk/store.py +827 -0
  68. sqlspec/adapters/bigquery/config.py +353 -0
  69. sqlspec/adapters/bigquery/core.cpython-310-aarch64-linux-gnu.so +0 -0
  70. sqlspec/adapters/bigquery/core.py +715 -0
  71. sqlspec/adapters/bigquery/data_dictionary.py +128 -0
  72. sqlspec/adapters/bigquery/driver.py +548 -0
  73. sqlspec/adapters/bigquery/events/__init__.py +5 -0
  74. sqlspec/adapters/bigquery/events/store.py +139 -0
  75. sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
  76. sqlspec/adapters/bigquery/litestar/store.py +325 -0
  77. sqlspec/adapters/bigquery/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
  78. sqlspec/adapters/bigquery/type_converter.py +107 -0
  79. sqlspec/adapters/cockroach_asyncpg/__init__.py +24 -0
  80. sqlspec/adapters/cockroach_asyncpg/_typing.py +72 -0
  81. sqlspec/adapters/cockroach_asyncpg/adk/__init__.py +3 -0
  82. sqlspec/adapters/cockroach_asyncpg/adk/store.py +410 -0
  83. sqlspec/adapters/cockroach_asyncpg/config.py +238 -0
  84. sqlspec/adapters/cockroach_asyncpg/core.cpython-310-aarch64-linux-gnu.so +0 -0
  85. sqlspec/adapters/cockroach_asyncpg/core.py +55 -0
  86. sqlspec/adapters/cockroach_asyncpg/data_dictionary.py +107 -0
  87. sqlspec/adapters/cockroach_asyncpg/driver.py +144 -0
  88. sqlspec/adapters/cockroach_asyncpg/events/__init__.py +3 -0
  89. sqlspec/adapters/cockroach_asyncpg/events/store.py +20 -0
  90. sqlspec/adapters/cockroach_asyncpg/litestar/__init__.py +3 -0
  91. sqlspec/adapters/cockroach_asyncpg/litestar/store.py +142 -0
  92. sqlspec/adapters/cockroach_psycopg/__init__.py +38 -0
  93. sqlspec/adapters/cockroach_psycopg/_typing.py +129 -0
  94. sqlspec/adapters/cockroach_psycopg/adk/__init__.py +13 -0
  95. sqlspec/adapters/cockroach_psycopg/adk/store.py +868 -0
  96. sqlspec/adapters/cockroach_psycopg/config.py +484 -0
  97. sqlspec/adapters/cockroach_psycopg/core.cpython-310-aarch64-linux-gnu.so +0 -0
  98. sqlspec/adapters/cockroach_psycopg/core.py +63 -0
  99. sqlspec/adapters/cockroach_psycopg/data_dictionary.py +215 -0
  100. sqlspec/adapters/cockroach_psycopg/driver.py +284 -0
  101. sqlspec/adapters/cockroach_psycopg/events/__init__.py +6 -0
  102. sqlspec/adapters/cockroach_psycopg/events/store.py +34 -0
  103. sqlspec/adapters/cockroach_psycopg/litestar/__init__.py +3 -0
  104. sqlspec/adapters/cockroach_psycopg/litestar/store.py +325 -0
  105. sqlspec/adapters/duckdb/__init__.py +25 -0
  106. sqlspec/adapters/duckdb/_typing.py +81 -0
  107. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  108. sqlspec/adapters/duckdb/adk/store.py +850 -0
  109. sqlspec/adapters/duckdb/config.py +463 -0
  110. sqlspec/adapters/duckdb/core.cpython-310-aarch64-linux-gnu.so +0 -0
  111. sqlspec/adapters/duckdb/core.py +257 -0
  112. sqlspec/adapters/duckdb/data_dictionary.py +140 -0
  113. sqlspec/adapters/duckdb/driver.py +430 -0
  114. sqlspec/adapters/duckdb/events/__init__.py +5 -0
  115. sqlspec/adapters/duckdb/events/store.py +57 -0
  116. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  117. sqlspec/adapters/duckdb/litestar/store.py +330 -0
  118. sqlspec/adapters/duckdb/pool.py +293 -0
  119. sqlspec/adapters/duckdb/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
  120. sqlspec/adapters/duckdb/type_converter.py +118 -0
  121. sqlspec/adapters/mock/__init__.py +72 -0
  122. sqlspec/adapters/mock/_typing.py +147 -0
  123. sqlspec/adapters/mock/config.py +483 -0
  124. sqlspec/adapters/mock/core.py +319 -0
  125. sqlspec/adapters/mock/data_dictionary.py +366 -0
  126. sqlspec/adapters/mock/driver.py +721 -0
  127. sqlspec/adapters/mysqlconnector/__init__.py +36 -0
  128. sqlspec/adapters/mysqlconnector/_typing.py +141 -0
  129. sqlspec/adapters/mysqlconnector/adk/__init__.py +15 -0
  130. sqlspec/adapters/mysqlconnector/adk/store.py +1060 -0
  131. sqlspec/adapters/mysqlconnector/config.py +394 -0
  132. sqlspec/adapters/mysqlconnector/core.cpython-310-aarch64-linux-gnu.so +0 -0
  133. sqlspec/adapters/mysqlconnector/core.py +303 -0
  134. sqlspec/adapters/mysqlconnector/data_dictionary.py +235 -0
  135. sqlspec/adapters/mysqlconnector/driver.py +483 -0
  136. sqlspec/adapters/mysqlconnector/events/__init__.py +8 -0
  137. sqlspec/adapters/mysqlconnector/events/store.py +98 -0
  138. sqlspec/adapters/mysqlconnector/litestar/__init__.py +5 -0
  139. sqlspec/adapters/mysqlconnector/litestar/store.py +426 -0
  140. sqlspec/adapters/oracledb/__init__.py +60 -0
  141. sqlspec/adapters/oracledb/_numpy_handlers.py +141 -0
  142. sqlspec/adapters/oracledb/_typing.py +182 -0
  143. sqlspec/adapters/oracledb/_uuid_handlers.py +166 -0
  144. sqlspec/adapters/oracledb/adk/__init__.py +10 -0
  145. sqlspec/adapters/oracledb/adk/store.py +2369 -0
  146. sqlspec/adapters/oracledb/config.py +550 -0
  147. sqlspec/adapters/oracledb/core.cpython-310-aarch64-linux-gnu.so +0 -0
  148. sqlspec/adapters/oracledb/core.py +543 -0
  149. sqlspec/adapters/oracledb/data_dictionary.py +536 -0
  150. sqlspec/adapters/oracledb/driver.py +1229 -0
  151. sqlspec/adapters/oracledb/events/__init__.py +16 -0
  152. sqlspec/adapters/oracledb/events/backend.py +347 -0
  153. sqlspec/adapters/oracledb/events/store.py +420 -0
  154. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  155. sqlspec/adapters/oracledb/litestar/store.py +781 -0
  156. sqlspec/adapters/oracledb/migrations.py +535 -0
  157. sqlspec/adapters/oracledb/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
  158. sqlspec/adapters/oracledb/type_converter.py +211 -0
  159. sqlspec/adapters/psqlpy/__init__.py +17 -0
  160. sqlspec/adapters/psqlpy/_typing.py +79 -0
  161. sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
  162. sqlspec/adapters/psqlpy/adk/store.py +766 -0
  163. sqlspec/adapters/psqlpy/config.py +304 -0
  164. sqlspec/adapters/psqlpy/core.cpython-310-aarch64-linux-gnu.so +0 -0
  165. sqlspec/adapters/psqlpy/core.py +480 -0
  166. sqlspec/adapters/psqlpy/data_dictionary.py +126 -0
  167. sqlspec/adapters/psqlpy/driver.py +438 -0
  168. sqlspec/adapters/psqlpy/events/__init__.py +6 -0
  169. sqlspec/adapters/psqlpy/events/backend.py +310 -0
  170. sqlspec/adapters/psqlpy/events/store.py +20 -0
  171. sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
  172. sqlspec/adapters/psqlpy/litestar/store.py +270 -0
  173. sqlspec/adapters/psqlpy/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
  174. sqlspec/adapters/psqlpy/type_converter.py +113 -0
  175. sqlspec/adapters/psycopg/__init__.py +32 -0
  176. sqlspec/adapters/psycopg/_typing.py +164 -0
  177. sqlspec/adapters/psycopg/adk/__init__.py +10 -0
  178. sqlspec/adapters/psycopg/adk/store.py +1387 -0
  179. sqlspec/adapters/psycopg/config.py +576 -0
  180. sqlspec/adapters/psycopg/core.cpython-310-aarch64-linux-gnu.so +0 -0
  181. sqlspec/adapters/psycopg/core.py +450 -0
  182. sqlspec/adapters/psycopg/data_dictionary.py +289 -0
  183. sqlspec/adapters/psycopg/driver.py +975 -0
  184. sqlspec/adapters/psycopg/events/__init__.py +20 -0
  185. sqlspec/adapters/psycopg/events/backend.py +458 -0
  186. sqlspec/adapters/psycopg/events/store.py +42 -0
  187. sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
  188. sqlspec/adapters/psycopg/litestar/store.py +552 -0
  189. sqlspec/adapters/psycopg/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
  190. sqlspec/adapters/psycopg/type_converter.py +93 -0
  191. sqlspec/adapters/pymysql/__init__.py +21 -0
  192. sqlspec/adapters/pymysql/_typing.py +71 -0
  193. sqlspec/adapters/pymysql/adk/__init__.py +5 -0
  194. sqlspec/adapters/pymysql/adk/store.py +540 -0
  195. sqlspec/adapters/pymysql/config.py +195 -0
  196. sqlspec/adapters/pymysql/core.cpython-310-aarch64-linux-gnu.so +0 -0
  197. sqlspec/adapters/pymysql/core.py +299 -0
  198. sqlspec/adapters/pymysql/data_dictionary.py +122 -0
  199. sqlspec/adapters/pymysql/driver.py +259 -0
  200. sqlspec/adapters/pymysql/events/__init__.py +5 -0
  201. sqlspec/adapters/pymysql/events/store.py +50 -0
  202. sqlspec/adapters/pymysql/litestar/__init__.py +5 -0
  203. sqlspec/adapters/pymysql/litestar/store.py +232 -0
  204. sqlspec/adapters/pymysql/pool.py +137 -0
  205. sqlspec/adapters/spanner/__init__.py +40 -0
  206. sqlspec/adapters/spanner/_typing.py +86 -0
  207. sqlspec/adapters/spanner/adk/__init__.py +5 -0
  208. sqlspec/adapters/spanner/adk/store.py +732 -0
  209. sqlspec/adapters/spanner/config.py +352 -0
  210. sqlspec/adapters/spanner/core.cpython-310-aarch64-linux-gnu.so +0 -0
  211. sqlspec/adapters/spanner/core.py +188 -0
  212. sqlspec/adapters/spanner/data_dictionary.py +120 -0
  213. sqlspec/adapters/spanner/dialect/__init__.py +6 -0
  214. sqlspec/adapters/spanner/dialect/_spangres.py +57 -0
  215. sqlspec/adapters/spanner/dialect/_spanner.py +130 -0
  216. sqlspec/adapters/spanner/driver.py +373 -0
  217. sqlspec/adapters/spanner/events/__init__.py +5 -0
  218. sqlspec/adapters/spanner/events/store.py +187 -0
  219. sqlspec/adapters/spanner/litestar/__init__.py +5 -0
  220. sqlspec/adapters/spanner/litestar/store.py +291 -0
  221. sqlspec/adapters/spanner/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
  222. sqlspec/adapters/spanner/type_converter.py +331 -0
  223. sqlspec/adapters/sqlite/__init__.py +19 -0
  224. sqlspec/adapters/sqlite/_typing.py +80 -0
  225. sqlspec/adapters/sqlite/adk/__init__.py +5 -0
  226. sqlspec/adapters/sqlite/adk/store.py +958 -0
  227. sqlspec/adapters/sqlite/config.py +280 -0
  228. sqlspec/adapters/sqlite/core.cpython-310-aarch64-linux-gnu.so +0 -0
  229. sqlspec/adapters/sqlite/core.py +312 -0
  230. sqlspec/adapters/sqlite/data_dictionary.py +202 -0
  231. sqlspec/adapters/sqlite/driver.py +359 -0
  232. sqlspec/adapters/sqlite/events/__init__.py +5 -0
  233. sqlspec/adapters/sqlite/events/store.py +20 -0
  234. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  235. sqlspec/adapters/sqlite/litestar/store.py +316 -0
  236. sqlspec/adapters/sqlite/pool.py +198 -0
  237. sqlspec/adapters/sqlite/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
  238. sqlspec/adapters/sqlite/type_converter.py +114 -0
  239. sqlspec/base.py +747 -0
  240. sqlspec/builder/__init__.py +179 -0
  241. sqlspec/builder/_base.cpython-310-aarch64-linux-gnu.so +0 -0
  242. sqlspec/builder/_base.py +1022 -0
  243. sqlspec/builder/_column.cpython-310-aarch64-linux-gnu.so +0 -0
  244. sqlspec/builder/_column.py +521 -0
  245. sqlspec/builder/_ddl.cpython-310-aarch64-linux-gnu.so +0 -0
  246. sqlspec/builder/_ddl.py +1642 -0
  247. sqlspec/builder/_delete.cpython-310-aarch64-linux-gnu.so +0 -0
  248. sqlspec/builder/_delete.py +95 -0
  249. sqlspec/builder/_dml.cpython-310-aarch64-linux-gnu.so +0 -0
  250. sqlspec/builder/_dml.py +365 -0
  251. sqlspec/builder/_explain.cpython-310-aarch64-linux-gnu.so +0 -0
  252. sqlspec/builder/_explain.py +579 -0
  253. sqlspec/builder/_expression_wrappers.cpython-310-aarch64-linux-gnu.so +0 -0
  254. sqlspec/builder/_expression_wrappers.py +46 -0
  255. sqlspec/builder/_factory.cpython-310-aarch64-linux-gnu.so +0 -0
  256. sqlspec/builder/_factory.py +1697 -0
  257. sqlspec/builder/_insert.cpython-310-aarch64-linux-gnu.so +0 -0
  258. sqlspec/builder/_insert.py +328 -0
  259. sqlspec/builder/_join.cpython-310-aarch64-linux-gnu.so +0 -0
  260. sqlspec/builder/_join.py +499 -0
  261. sqlspec/builder/_merge.cpython-310-aarch64-linux-gnu.so +0 -0
  262. sqlspec/builder/_merge.py +821 -0
  263. sqlspec/builder/_parsing_utils.cpython-310-aarch64-linux-gnu.so +0 -0
  264. sqlspec/builder/_parsing_utils.py +297 -0
  265. sqlspec/builder/_select.cpython-310-aarch64-linux-gnu.so +0 -0
  266. sqlspec/builder/_select.py +1660 -0
  267. sqlspec/builder/_temporal.cpython-310-aarch64-linux-gnu.so +0 -0
  268. sqlspec/builder/_temporal.py +139 -0
  269. sqlspec/builder/_update.cpython-310-aarch64-linux-gnu.so +0 -0
  270. sqlspec/builder/_update.py +173 -0
  271. sqlspec/builder/_vector_expressions.py +267 -0
  272. sqlspec/cli.py +911 -0
  273. sqlspec/config.py +1755 -0
  274. sqlspec/core/__init__.py +374 -0
  275. sqlspec/core/_correlation.cpython-310-aarch64-linux-gnu.so +0 -0
  276. sqlspec/core/_correlation.py +176 -0
  277. sqlspec/core/cache.cpython-310-aarch64-linux-gnu.so +0 -0
  278. sqlspec/core/cache.py +1069 -0
  279. sqlspec/core/compiler.cpython-310-aarch64-linux-gnu.so +0 -0
  280. sqlspec/core/compiler.py +954 -0
  281. sqlspec/core/explain.cpython-310-aarch64-linux-gnu.so +0 -0
  282. sqlspec/core/explain.py +275 -0
  283. sqlspec/core/filters.cpython-310-aarch64-linux-gnu.so +0 -0
  284. sqlspec/core/filters.py +952 -0
  285. sqlspec/core/hashing.cpython-310-aarch64-linux-gnu.so +0 -0
  286. sqlspec/core/hashing.py +262 -0
  287. sqlspec/core/metrics.cpython-310-aarch64-linux-gnu.so +0 -0
  288. sqlspec/core/metrics.py +83 -0
  289. sqlspec/core/parameters/__init__.py +71 -0
  290. sqlspec/core/parameters/_alignment.cpython-310-aarch64-linux-gnu.so +0 -0
  291. sqlspec/core/parameters/_alignment.py +270 -0
  292. sqlspec/core/parameters/_converter.cpython-310-aarch64-linux-gnu.so +0 -0
  293. sqlspec/core/parameters/_converter.py +543 -0
  294. sqlspec/core/parameters/_processor.cpython-310-aarch64-linux-gnu.so +0 -0
  295. sqlspec/core/parameters/_processor.py +505 -0
  296. sqlspec/core/parameters/_registry.cpython-310-aarch64-linux-gnu.so +0 -0
  297. sqlspec/core/parameters/_registry.py +206 -0
  298. sqlspec/core/parameters/_transformers.cpython-310-aarch64-linux-gnu.so +0 -0
  299. sqlspec/core/parameters/_transformers.py +292 -0
  300. sqlspec/core/parameters/_types.cpython-310-aarch64-linux-gnu.so +0 -0
  301. sqlspec/core/parameters/_types.py +499 -0
  302. sqlspec/core/parameters/_validator.cpython-310-aarch64-linux-gnu.so +0 -0
  303. sqlspec/core/parameters/_validator.py +180 -0
  304. sqlspec/core/pipeline.cpython-310-aarch64-linux-gnu.so +0 -0
  305. sqlspec/core/pipeline.py +319 -0
  306. sqlspec/core/query_modifiers.cpython-310-aarch64-linux-gnu.so +0 -0
  307. sqlspec/core/query_modifiers.py +437 -0
  308. sqlspec/core/result/__init__.py +23 -0
  309. sqlspec/core/result/_base.cpython-310-aarch64-linux-gnu.so +0 -0
  310. sqlspec/core/result/_base.py +1121 -0
  311. sqlspec/core/result/_io.cpython-310-aarch64-linux-gnu.so +0 -0
  312. sqlspec/core/result/_io.py +28 -0
  313. sqlspec/core/splitter.cpython-310-aarch64-linux-gnu.so +0 -0
  314. sqlspec/core/splitter.py +966 -0
  315. sqlspec/core/stack.cpython-310-aarch64-linux-gnu.so +0 -0
  316. sqlspec/core/stack.py +163 -0
  317. sqlspec/core/statement.cpython-310-aarch64-linux-gnu.so +0 -0
  318. sqlspec/core/statement.py +1503 -0
  319. sqlspec/core/type_converter.cpython-310-aarch64-linux-gnu.so +0 -0
  320. sqlspec/core/type_converter.py +339 -0
  321. sqlspec/data_dictionary/__init__.py +22 -0
  322. sqlspec/data_dictionary/_loader.py +123 -0
  323. sqlspec/data_dictionary/_registry.cpython-310-aarch64-linux-gnu.so +0 -0
  324. sqlspec/data_dictionary/_registry.py +74 -0
  325. sqlspec/data_dictionary/_types.cpython-310-aarch64-linux-gnu.so +0 -0
  326. sqlspec/data_dictionary/_types.py +121 -0
  327. sqlspec/data_dictionary/dialects/__init__.py +21 -0
  328. sqlspec/data_dictionary/dialects/bigquery.cpython-310-aarch64-linux-gnu.so +0 -0
  329. sqlspec/data_dictionary/dialects/bigquery.py +49 -0
  330. sqlspec/data_dictionary/dialects/cockroachdb.cpython-310-aarch64-linux-gnu.so +0 -0
  331. sqlspec/data_dictionary/dialects/cockroachdb.py +43 -0
  332. sqlspec/data_dictionary/dialects/duckdb.cpython-310-aarch64-linux-gnu.so +0 -0
  333. sqlspec/data_dictionary/dialects/duckdb.py +47 -0
  334. sqlspec/data_dictionary/dialects/mysql.cpython-310-aarch64-linux-gnu.so +0 -0
  335. sqlspec/data_dictionary/dialects/mysql.py +42 -0
  336. sqlspec/data_dictionary/dialects/oracle.cpython-310-aarch64-linux-gnu.so +0 -0
  337. sqlspec/data_dictionary/dialects/oracle.py +34 -0
  338. sqlspec/data_dictionary/dialects/postgres.cpython-310-aarch64-linux-gnu.so +0 -0
  339. sqlspec/data_dictionary/dialects/postgres.py +46 -0
  340. sqlspec/data_dictionary/dialects/spanner.cpython-310-aarch64-linux-gnu.so +0 -0
  341. sqlspec/data_dictionary/dialects/spanner.py +37 -0
  342. sqlspec/data_dictionary/dialects/sqlite.cpython-310-aarch64-linux-gnu.so +0 -0
  343. sqlspec/data_dictionary/dialects/sqlite.py +42 -0
  344. sqlspec/data_dictionary/sql/.gitkeep +0 -0
  345. sqlspec/data_dictionary/sql/bigquery/columns.sql +23 -0
  346. sqlspec/data_dictionary/sql/bigquery/foreign_keys.sql +34 -0
  347. sqlspec/data_dictionary/sql/bigquery/indexes.sql +19 -0
  348. sqlspec/data_dictionary/sql/bigquery/tables.sql +33 -0
  349. sqlspec/data_dictionary/sql/bigquery/version.sql +3 -0
  350. sqlspec/data_dictionary/sql/cockroachdb/columns.sql +34 -0
  351. sqlspec/data_dictionary/sql/cockroachdb/foreign_keys.sql +40 -0
  352. sqlspec/data_dictionary/sql/cockroachdb/indexes.sql +32 -0
  353. sqlspec/data_dictionary/sql/cockroachdb/tables.sql +44 -0
  354. sqlspec/data_dictionary/sql/cockroachdb/version.sql +3 -0
  355. sqlspec/data_dictionary/sql/duckdb/columns.sql +23 -0
  356. sqlspec/data_dictionary/sql/duckdb/foreign_keys.sql +36 -0
  357. sqlspec/data_dictionary/sql/duckdb/indexes.sql +19 -0
  358. sqlspec/data_dictionary/sql/duckdb/tables.sql +38 -0
  359. sqlspec/data_dictionary/sql/duckdb/version.sql +3 -0
  360. sqlspec/data_dictionary/sql/mysql/columns.sql +23 -0
  361. sqlspec/data_dictionary/sql/mysql/foreign_keys.sql +28 -0
  362. sqlspec/data_dictionary/sql/mysql/indexes.sql +26 -0
  363. sqlspec/data_dictionary/sql/mysql/tables.sql +33 -0
  364. sqlspec/data_dictionary/sql/mysql/version.sql +3 -0
  365. sqlspec/data_dictionary/sql/oracle/columns.sql +23 -0
  366. sqlspec/data_dictionary/sql/oracle/foreign_keys.sql +48 -0
  367. sqlspec/data_dictionary/sql/oracle/indexes.sql +44 -0
  368. sqlspec/data_dictionary/sql/oracle/tables.sql +25 -0
  369. sqlspec/data_dictionary/sql/oracle/version.sql +20 -0
  370. sqlspec/data_dictionary/sql/postgres/columns.sql +34 -0
  371. sqlspec/data_dictionary/sql/postgres/foreign_keys.sql +40 -0
  372. sqlspec/data_dictionary/sql/postgres/indexes.sql +56 -0
  373. sqlspec/data_dictionary/sql/postgres/tables.sql +44 -0
  374. sqlspec/data_dictionary/sql/postgres/version.sql +3 -0
  375. sqlspec/data_dictionary/sql/spanner/columns.sql +23 -0
  376. sqlspec/data_dictionary/sql/spanner/foreign_keys.sql +70 -0
  377. sqlspec/data_dictionary/sql/spanner/indexes.sql +30 -0
  378. sqlspec/data_dictionary/sql/spanner/tables.sql +9 -0
  379. sqlspec/data_dictionary/sql/spanner/version.sql +3 -0
  380. sqlspec/data_dictionary/sql/sqlite/columns.sql +23 -0
  381. sqlspec/data_dictionary/sql/sqlite/foreign_keys.sql +22 -0
  382. sqlspec/data_dictionary/sql/sqlite/indexes.sql +7 -0
  383. sqlspec/data_dictionary/sql/sqlite/tables.sql +28 -0
  384. sqlspec/data_dictionary/sql/sqlite/version.sql +3 -0
  385. sqlspec/driver/__init__.py +32 -0
  386. sqlspec/driver/_async.cpython-310-aarch64-linux-gnu.so +0 -0
  387. sqlspec/driver/_async.py +1737 -0
  388. sqlspec/driver/_common.cpython-310-aarch64-linux-gnu.so +0 -0
  389. sqlspec/driver/_common.py +1478 -0
  390. sqlspec/driver/_sql_helpers.cpython-310-aarch64-linux-gnu.so +0 -0
  391. sqlspec/driver/_sql_helpers.py +148 -0
  392. sqlspec/driver/_storage_helpers.cpython-310-aarch64-linux-gnu.so +0 -0
  393. sqlspec/driver/_storage_helpers.py +144 -0
  394. sqlspec/driver/_sync.cpython-310-aarch64-linux-gnu.so +0 -0
  395. sqlspec/driver/_sync.py +1710 -0
  396. sqlspec/exceptions.py +338 -0
  397. sqlspec/extensions/__init__.py +0 -0
  398. sqlspec/extensions/adk/__init__.py +70 -0
  399. sqlspec/extensions/adk/_types.py +51 -0
  400. sqlspec/extensions/adk/converters.py +172 -0
  401. sqlspec/extensions/adk/memory/__init__.py +69 -0
  402. sqlspec/extensions/adk/memory/_types.py +30 -0
  403. sqlspec/extensions/adk/memory/converters.py +149 -0
  404. sqlspec/extensions/adk/memory/service.py +217 -0
  405. sqlspec/extensions/adk/memory/store.py +569 -0
  406. sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +246 -0
  407. sqlspec/extensions/adk/migrations/__init__.py +0 -0
  408. sqlspec/extensions/adk/service.py +225 -0
  409. sqlspec/extensions/adk/store.py +567 -0
  410. sqlspec/extensions/events/__init__.py +51 -0
  411. sqlspec/extensions/events/_channel.py +703 -0
  412. sqlspec/extensions/events/_hints.py +45 -0
  413. sqlspec/extensions/events/_models.py +23 -0
  414. sqlspec/extensions/events/_payload.py +69 -0
  415. sqlspec/extensions/events/_protocols.py +134 -0
  416. sqlspec/extensions/events/_queue.py +461 -0
  417. sqlspec/extensions/events/_store.py +209 -0
  418. sqlspec/extensions/events/migrations/0001_create_event_queue.py +59 -0
  419. sqlspec/extensions/events/migrations/__init__.py +3 -0
  420. sqlspec/extensions/fastapi/__init__.py +19 -0
  421. sqlspec/extensions/fastapi/extension.py +351 -0
  422. sqlspec/extensions/fastapi/providers.py +607 -0
  423. sqlspec/extensions/flask/__init__.py +37 -0
  424. sqlspec/extensions/flask/_state.py +76 -0
  425. sqlspec/extensions/flask/_utils.py +71 -0
  426. sqlspec/extensions/flask/extension.py +519 -0
  427. sqlspec/extensions/litestar/__init__.py +28 -0
  428. sqlspec/extensions/litestar/_utils.py +52 -0
  429. sqlspec/extensions/litestar/channels.py +165 -0
  430. sqlspec/extensions/litestar/cli.py +102 -0
  431. sqlspec/extensions/litestar/config.py +90 -0
  432. sqlspec/extensions/litestar/handlers.py +316 -0
  433. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  434. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  435. sqlspec/extensions/litestar/plugin.py +671 -0
  436. sqlspec/extensions/litestar/providers.py +526 -0
  437. sqlspec/extensions/litestar/store.py +296 -0
  438. sqlspec/extensions/otel/__init__.py +58 -0
  439. sqlspec/extensions/prometheus/__init__.py +113 -0
  440. sqlspec/extensions/starlette/__init__.py +19 -0
  441. sqlspec/extensions/starlette/_state.py +30 -0
  442. sqlspec/extensions/starlette/_utils.py +96 -0
  443. sqlspec/extensions/starlette/extension.py +346 -0
  444. sqlspec/extensions/starlette/middleware.py +235 -0
  445. sqlspec/loader.cpython-310-aarch64-linux-gnu.so +0 -0
  446. sqlspec/loader.py +702 -0
  447. sqlspec/migrations/__init__.py +36 -0
  448. sqlspec/migrations/base.py +731 -0
  449. sqlspec/migrations/commands.py +1232 -0
  450. sqlspec/migrations/context.py +157 -0
  451. sqlspec/migrations/fix.py +204 -0
  452. sqlspec/migrations/loaders.py +443 -0
  453. sqlspec/migrations/runner.py +1172 -0
  454. sqlspec/migrations/templates.py +234 -0
  455. sqlspec/migrations/tracker.py +611 -0
  456. sqlspec/migrations/utils.py +256 -0
  457. sqlspec/migrations/validation.py +207 -0
  458. sqlspec/migrations/version.py +446 -0
  459. sqlspec/observability/__init__.py +55 -0
  460. sqlspec/observability/_common.cpython-310-aarch64-linux-gnu.so +0 -0
  461. sqlspec/observability/_common.py +77 -0
  462. sqlspec/observability/_config.cpython-310-aarch64-linux-gnu.so +0 -0
  463. sqlspec/observability/_config.py +348 -0
  464. sqlspec/observability/_diagnostics.cpython-310-aarch64-linux-gnu.so +0 -0
  465. sqlspec/observability/_diagnostics.py +74 -0
  466. sqlspec/observability/_dispatcher.cpython-310-aarch64-linux-gnu.so +0 -0
  467. sqlspec/observability/_dispatcher.py +152 -0
  468. sqlspec/observability/_formatters/__init__.py +13 -0
  469. sqlspec/observability/_formatters/_aws.cpython-310-aarch64-linux-gnu.so +0 -0
  470. sqlspec/observability/_formatters/_aws.py +102 -0
  471. sqlspec/observability/_formatters/_azure.cpython-310-aarch64-linux-gnu.so +0 -0
  472. sqlspec/observability/_formatters/_azure.py +96 -0
  473. sqlspec/observability/_formatters/_base.cpython-310-aarch64-linux-gnu.so +0 -0
  474. sqlspec/observability/_formatters/_base.py +57 -0
  475. sqlspec/observability/_formatters/_gcp.cpython-310-aarch64-linux-gnu.so +0 -0
  476. sqlspec/observability/_formatters/_gcp.py +131 -0
  477. sqlspec/observability/_formatting.py +58 -0
  478. sqlspec/observability/_observer.cpython-310-aarch64-linux-gnu.so +0 -0
  479. sqlspec/observability/_observer.py +357 -0
  480. sqlspec/observability/_runtime.cpython-310-aarch64-linux-gnu.so +0 -0
  481. sqlspec/observability/_runtime.py +420 -0
  482. sqlspec/observability/_sampling.cpython-310-aarch64-linux-gnu.so +0 -0
  483. sqlspec/observability/_sampling.py +188 -0
  484. sqlspec/observability/_spans.cpython-310-aarch64-linux-gnu.so +0 -0
  485. sqlspec/observability/_spans.py +161 -0
  486. sqlspec/protocols.py +916 -0
  487. sqlspec/py.typed +0 -0
  488. sqlspec/storage/__init__.py +48 -0
  489. sqlspec/storage/_utils.py +104 -0
  490. sqlspec/storage/backends/__init__.py +1 -0
  491. sqlspec/storage/backends/base.py +253 -0
  492. sqlspec/storage/backends/fsspec.py +529 -0
  493. sqlspec/storage/backends/local.py +441 -0
  494. sqlspec/storage/backends/obstore.py +916 -0
  495. sqlspec/storage/errors.py +104 -0
  496. sqlspec/storage/pipeline.py +582 -0
  497. sqlspec/storage/registry.py +301 -0
  498. sqlspec/typing.py +395 -0
  499. sqlspec/utils/__init__.py +7 -0
  500. sqlspec/utils/arrow_helpers.py +318 -0
  501. sqlspec/utils/config_tools.py +332 -0
  502. sqlspec/utils/correlation.cpython-310-aarch64-linux-gnu.so +0 -0
  503. sqlspec/utils/correlation.py +134 -0
  504. sqlspec/utils/deprecation.py +190 -0
  505. sqlspec/utils/fixtures.cpython-310-aarch64-linux-gnu.so +0 -0
  506. sqlspec/utils/fixtures.py +258 -0
  507. sqlspec/utils/logging.py +222 -0
  508. sqlspec/utils/module_loader.py +306 -0
  509. sqlspec/utils/portal.cpython-310-aarch64-linux-gnu.so +0 -0
  510. sqlspec/utils/portal.py +375 -0
  511. sqlspec/utils/schema.cpython-310-aarch64-linux-gnu.so +0 -0
  512. sqlspec/utils/schema.py +485 -0
  513. sqlspec/utils/serializers.cpython-310-aarch64-linux-gnu.so +0 -0
  514. sqlspec/utils/serializers.py +408 -0
  515. sqlspec/utils/singleton.cpython-310-aarch64-linux-gnu.so +0 -0
  516. sqlspec/utils/singleton.py +41 -0
  517. sqlspec/utils/sync_tools.cpython-310-aarch64-linux-gnu.so +0 -0
  518. sqlspec/utils/sync_tools.py +311 -0
  519. sqlspec/utils/text.cpython-310-aarch64-linux-gnu.so +0 -0
  520. sqlspec/utils/text.py +108 -0
  521. sqlspec/utils/type_converters.cpython-310-aarch64-linux-gnu.so +0 -0
  522. sqlspec/utils/type_converters.py +128 -0
  523. sqlspec/utils/type_guards.cpython-310-aarch64-linux-gnu.so +0 -0
  524. sqlspec/utils/type_guards.py +1360 -0
  525. sqlspec/utils/uuids.cpython-310-aarch64-linux-gnu.so +0 -0
  526. sqlspec/utils/uuids.py +225 -0
  527. sqlspec-0.36.0.dist-info/METADATA +205 -0
  528. sqlspec-0.36.0.dist-info/RECORD +531 -0
  529. sqlspec-0.36.0.dist-info/WHEEL +7 -0
  530. sqlspec-0.36.0.dist-info/entry_points.txt +2 -0
  531. sqlspec-0.36.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,1387 @@
1
+ """Psycopg ADK store for Google Agent Development Kit session/event storage."""
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from psycopg import errors
6
+ from psycopg import sql as pg_sql
7
+ from psycopg.types.json import Jsonb
8
+
9
+ from sqlspec.extensions.adk import BaseAsyncADKStore, BaseSyncADKStore, EventRecord, SessionRecord
10
+ from sqlspec.extensions.adk.memory.store import BaseAsyncADKMemoryStore, BaseSyncADKMemoryStore
11
+ from sqlspec.utils.logging import get_logger
12
+
13
+ if TYPE_CHECKING:
14
+ from datetime import datetime
15
+
16
+ from sqlspec.adapters.psycopg.config import PsycopgAsyncConfig, PsycopgSyncConfig
17
+ from sqlspec.extensions.adk import MemoryRecord
18
+
19
+
20
+ __all__ = ("PsycopgAsyncADKMemoryStore", "PsycopgAsyncADKStore", "PsycopgSyncADKMemoryStore", "PsycopgSyncADKStore")
21
+
22
+ logger = get_logger("sqlspec.adapters.psycopg.adk.store")
23
+
24
+
25
+ def _build_insert_params(entry: "MemoryRecord") -> "tuple[object, ...]":
26
+ return (
27
+ entry["id"],
28
+ entry["session_id"],
29
+ entry["app_name"],
30
+ entry["user_id"],
31
+ entry["event_id"],
32
+ entry["author"],
33
+ entry["timestamp"],
34
+ Jsonb(entry["content_json"]),
35
+ entry["content_text"],
36
+ Jsonb(entry["metadata_json"]) if entry["metadata_json"] is not None else None,
37
+ entry["inserted_at"],
38
+ )
39
+
40
+
41
+ def _build_insert_params_with_owner(entry: "MemoryRecord", owner_id: "object | None") -> "tuple[object, ...]":
42
+ return (
43
+ entry["id"],
44
+ entry["session_id"],
45
+ entry["app_name"],
46
+ entry["user_id"],
47
+ entry["event_id"],
48
+ entry["author"],
49
+ owner_id,
50
+ entry["timestamp"],
51
+ Jsonb(entry["content_json"]),
52
+ entry["content_text"],
53
+ Jsonb(entry["metadata_json"]) if entry["metadata_json"] is not None else None,
54
+ entry["inserted_at"],
55
+ )
56
+
57
+
58
+ class PsycopgAsyncADKStore(BaseAsyncADKStore["PsycopgAsyncConfig"]):
59
+ """PostgreSQL ADK store using Psycopg3 driver.
60
+
61
+ Implements session and event storage for Google Agent Development Kit
62
+ using PostgreSQL via psycopg3 with native async/await support.
63
+
64
+ Provides:
65
+ - Session state management with JSONB storage and merge operations
66
+ - Event history tracking with BYTEA-serialized actions
67
+ - Microsecond-precision timestamps with TIMESTAMPTZ
68
+ - Foreign key constraints with cascade delete
69
+ - Efficient upserts using ON CONFLICT
70
+ - GIN indexes for JSONB queries
71
+ - HOT updates with FILLFACTOR 80
72
+
73
+ Args:
74
+ config: PsycopgAsyncConfig with extension_config["adk"] settings.
75
+
76
+ Example:
77
+ from sqlspec.adapters.psycopg import PsycopgAsyncConfig
78
+ from sqlspec.adapters.psycopg.adk import PsycopgAsyncADKStore
79
+
80
+ config = PsycopgAsyncConfig(
81
+ connection_config={"conninfo": "postgresql://..."},
82
+ extension_config={
83
+ "adk": {
84
+ "session_table": "my_sessions",
85
+ "events_table": "my_events",
86
+ "owner_id_column": "tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE CASCADE"
87
+ }
88
+ }
89
+ )
90
+ store = PsycopgAsyncADKStore(config)
91
+ await store.ensure_tables()
92
+
93
+ Notes:
94
+ - PostgreSQL JSONB type used for state (more efficient than JSON)
95
+ - Psycopg requires wrapping dicts with Jsonb() for type safety
96
+ - TIMESTAMPTZ provides timezone-aware microsecond precision
97
+ - State merging uses `state || $1::jsonb` operator for efficiency
98
+ - BYTEA for pre-serialized actions from Google ADK
99
+ - GIN index on state for JSONB queries (partial index)
100
+ - FILLFACTOR 80 leaves space for HOT updates
101
+ - Parameter style: $1, $2, $3 (PostgreSQL numeric placeholders)
102
+ - Configuration is read from config.extension_config["adk"]
103
+ """
104
+
105
+ __slots__ = ()
106
+
107
+ def __init__(self, config: "PsycopgAsyncConfig") -> None:
108
+ """Initialize Psycopg ADK store.
109
+
110
+ Args:
111
+ config: PsycopgAsyncConfig 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
+ """
119
+ super().__init__(config)
120
+
121
+ async def _get_create_sessions_table_sql(self) -> str:
122
+ """Get PostgreSQL CREATE TABLE SQL for sessions.
123
+
124
+ Returns:
125
+ SQL statement to create adk_sessions table with indexes.
126
+
127
+ Notes:
128
+ - VARCHAR(128) for IDs and names (sufficient for UUIDs and app names)
129
+ - JSONB type for state storage with default empty object
130
+ - TIMESTAMPTZ with microsecond precision
131
+ - FILLFACTOR 80 for HOT updates (reduces table bloat)
132
+ - Composite index on (app_name, user_id) for listing
133
+ - Index on update_time DESC for recent session queries
134
+ - Partial GIN index on state for JSONB queries (only non-empty)
135
+ - Optional owner ID column for multi-tenancy or user references
136
+ """
137
+ owner_id_line = ""
138
+ if self._owner_id_column_ddl:
139
+ owner_id_line = f",\n {self._owner_id_column_ddl}"
140
+
141
+ return f"""
142
+ CREATE TABLE IF NOT EXISTS {self._session_table} (
143
+ id VARCHAR(128) PRIMARY KEY,
144
+ app_name VARCHAR(128) NOT NULL,
145
+ user_id VARCHAR(128) NOT NULL{owner_id_line},
146
+ state JSONB NOT NULL DEFAULT '{{}}'::jsonb,
147
+ create_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
148
+ update_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
149
+ ) WITH (fillfactor = 80);
150
+
151
+ CREATE INDEX IF NOT EXISTS idx_{self._session_table}_app_user
152
+ ON {self._session_table}(app_name, user_id);
153
+
154
+ CREATE INDEX IF NOT EXISTS idx_{self._session_table}_update_time
155
+ ON {self._session_table}(update_time DESC);
156
+
157
+ CREATE INDEX IF NOT EXISTS idx_{self._session_table}_state
158
+ ON {self._session_table} USING GIN (state)
159
+ WHERE state != '{{}}'::jsonb;
160
+ """
161
+
162
+ async def _get_create_events_table_sql(self) -> str:
163
+ """Get PostgreSQL CREATE TABLE SQL for events.
164
+
165
+ Returns:
166
+ SQL statement to create adk_events table with indexes.
167
+
168
+ Notes:
169
+ - VARCHAR sizes: id(128), session_id(128), invocation_id(256), author(256),
170
+ branch(256), error_code(256), error_message(1024)
171
+ - BYTEA for pickled actions (no size limit)
172
+ - JSONB for content, grounding_metadata, custom_metadata, long_running_tool_ids_json
173
+ - BOOLEAN for partial, turn_complete, interrupted
174
+ - Foreign key to sessions with CASCADE delete
175
+ - Index on (session_id, timestamp ASC) for ordered event retrieval
176
+ """
177
+ return f"""
178
+ CREATE TABLE IF NOT EXISTS {self._events_table} (
179
+ id VARCHAR(128) PRIMARY KEY,
180
+ session_id VARCHAR(128) NOT NULL,
181
+ app_name VARCHAR(128) NOT NULL,
182
+ user_id VARCHAR(128) NOT NULL,
183
+ invocation_id VARCHAR(256),
184
+ author VARCHAR(256),
185
+ actions BYTEA,
186
+ long_running_tool_ids_json JSONB,
187
+ branch VARCHAR(256),
188
+ timestamp TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
189
+ content JSONB,
190
+ grounding_metadata JSONB,
191
+ custom_metadata JSONB,
192
+ partial BOOLEAN,
193
+ turn_complete BOOLEAN,
194
+ interrupted BOOLEAN,
195
+ error_code VARCHAR(256),
196
+ error_message VARCHAR(1024),
197
+ FOREIGN KEY (session_id) REFERENCES {self._session_table}(id) ON DELETE CASCADE
198
+ );
199
+
200
+ CREATE INDEX IF NOT EXISTS idx_{self._events_table}_session
201
+ ON {self._events_table}(session_id, timestamp ASC);
202
+ """
203
+
204
+ def _get_drop_tables_sql(self) -> "list[str]":
205
+ """Get PostgreSQL DROP TABLE SQL statements.
206
+
207
+ Returns:
208
+ List of SQL statements to drop tables and indexes.
209
+
210
+ Notes:
211
+ Order matters: drop events table (child) before sessions (parent).
212
+ PostgreSQL automatically drops indexes when dropping tables.
213
+ """
214
+ return [f"DROP TABLE IF EXISTS {self._events_table}", f"DROP TABLE IF EXISTS {self._session_table}"]
215
+
216
+ async def create_tables(self) -> None:
217
+ """Create both sessions and events tables if they don't exist."""
218
+ async with self._config.provide_session() as driver:
219
+ await driver.execute_script(await self._get_create_sessions_table_sql())
220
+ await driver.execute_script(await self._get_create_events_table_sql())
221
+
222
+ async def create_session(
223
+ self, session_id: str, app_name: str, user_id: str, state: "dict[str, Any]", owner_id: "Any | None" = None
224
+ ) -> SessionRecord:
225
+ """Create a new session.
226
+
227
+ Args:
228
+ session_id: Unique session identifier.
229
+ app_name: Application name.
230
+ user_id: User identifier.
231
+ state: Initial session state.
232
+ owner_id: Optional owner ID value for owner_id_column (if configured).
233
+
234
+ Returns:
235
+ Created session record.
236
+
237
+ Notes:
238
+ Uses CURRENT_TIMESTAMP for create_time and update_time.
239
+ State is wrapped with Jsonb() for PostgreSQL type safety.
240
+ If owner_id_column is configured, owner_id value must be provided.
241
+ """
242
+ params: tuple[Any, ...]
243
+ if self._owner_id_column_name:
244
+ query = pg_sql.SQL("""
245
+ INSERT INTO {table} (id, app_name, user_id, {owner_id_col}, state, create_time, update_time)
246
+ VALUES (%s, %s, %s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
247
+ """).format(
248
+ table=pg_sql.Identifier(self._session_table), owner_id_col=pg_sql.Identifier(self._owner_id_column_name)
249
+ )
250
+ params = (session_id, app_name, user_id, owner_id, Jsonb(state))
251
+ else:
252
+ query = pg_sql.SQL("""
253
+ INSERT INTO {table} (id, app_name, user_id, state, create_time, update_time)
254
+ VALUES (%s, %s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
255
+ """).format(table=pg_sql.Identifier(self._session_table))
256
+ params = (session_id, app_name, user_id, Jsonb(state))
257
+
258
+ async with self._config.provide_connection() as conn, conn.cursor() as cur:
259
+ await cur.execute(query, params)
260
+
261
+ return await self.get_session(session_id) # type: ignore[return-value]
262
+
263
+ async def get_session(self, session_id: str) -> "SessionRecord | None":
264
+ """Get session by ID.
265
+
266
+ Args:
267
+ session_id: Session identifier.
268
+
269
+ Returns:
270
+ Session record or None if not found.
271
+
272
+ Notes:
273
+ PostgreSQL returns datetime objects for TIMESTAMPTZ columns.
274
+ JSONB is automatically deserialized by psycopg to Python dict.
275
+ """
276
+ query = pg_sql.SQL("""
277
+ SELECT id, app_name, user_id, state, create_time, update_time
278
+ FROM {table}
279
+ WHERE id = %s
280
+ """).format(table=pg_sql.Identifier(self._session_table))
281
+
282
+ try:
283
+ async with self._config.provide_connection() as conn, conn.cursor() as cur:
284
+ await cur.execute(query, (session_id,))
285
+ row = await cur.fetchone()
286
+
287
+ if row is None:
288
+ return None
289
+
290
+ return SessionRecord(
291
+ id=row["id"],
292
+ app_name=row["app_name"],
293
+ user_id=row["user_id"],
294
+ state=row["state"],
295
+ create_time=row["create_time"],
296
+ update_time=row["update_time"],
297
+ )
298
+ except errors.UndefinedTable:
299
+ return None
300
+
301
+ async def update_session_state(self, session_id: str, state: "dict[str, Any]") -> None:
302
+ """Update session state.
303
+
304
+ Args:
305
+ session_id: Session identifier.
306
+ state: New state dictionary (replaces existing state).
307
+
308
+ Notes:
309
+ This replaces the entire state dictionary.
310
+ Uses CURRENT_TIMESTAMP for update_time.
311
+ State is wrapped with Jsonb() for PostgreSQL type safety.
312
+ """
313
+ query = pg_sql.SQL("""
314
+ UPDATE {table}
315
+ SET state = %s, update_time = CURRENT_TIMESTAMP
316
+ WHERE id = %s
317
+ """).format(table=pg_sql.Identifier(self._session_table))
318
+
319
+ async with self._config.provide_connection() as conn, conn.cursor() as cur:
320
+ await cur.execute(query, (Jsonb(state), session_id))
321
+
322
+ async def delete_session(self, session_id: str) -> None:
323
+ """Delete session and all associated events (cascade).
324
+
325
+ Args:
326
+ session_id: Session identifier.
327
+
328
+ Notes:
329
+ Foreign key constraint ensures events are cascade-deleted.
330
+ """
331
+ query = pg_sql.SQL("DELETE FROM {table} WHERE id = %s").format(table=pg_sql.Identifier(self._session_table))
332
+
333
+ async with self._config.provide_connection() as conn, conn.cursor() as cur:
334
+ await cur.execute(query, (session_id,))
335
+
336
+ async def list_sessions(self, app_name: str, user_id: str | None = None) -> "list[SessionRecord]":
337
+ """List sessions for an app, optionally filtered by user.
338
+
339
+ Args:
340
+ app_name: Application name.
341
+ user_id: User identifier. If None, lists all sessions for the app.
342
+
343
+ Returns:
344
+ List of session records ordered by update_time DESC.
345
+
346
+ Notes:
347
+ Uses composite index on (app_name, user_id) when user_id is provided.
348
+ """
349
+ if user_id is None:
350
+ query = pg_sql.SQL("""
351
+ SELECT id, app_name, user_id, state, create_time, update_time
352
+ FROM {table}
353
+ WHERE app_name = %s
354
+ ORDER BY update_time DESC
355
+ """).format(table=pg_sql.Identifier(self._session_table))
356
+ params: tuple[str, ...] = (app_name,)
357
+ else:
358
+ query = pg_sql.SQL("""
359
+ SELECT id, app_name, user_id, state, create_time, update_time
360
+ FROM {table}
361
+ WHERE app_name = %s AND user_id = %s
362
+ ORDER BY update_time DESC
363
+ """).format(table=pg_sql.Identifier(self._session_table))
364
+ params = (app_name, user_id)
365
+
366
+ try:
367
+ async with self._config.provide_connection() as conn, conn.cursor() as cur:
368
+ await cur.execute(query, params)
369
+ rows = await cur.fetchall()
370
+
371
+ return [
372
+ SessionRecord(
373
+ id=row["id"],
374
+ app_name=row["app_name"],
375
+ user_id=row["user_id"],
376
+ state=row["state"],
377
+ create_time=row["create_time"],
378
+ update_time=row["update_time"],
379
+ )
380
+ for row in rows
381
+ ]
382
+ except errors.UndefinedTable:
383
+ return []
384
+
385
+ async def append_event(self, event_record: EventRecord) -> None:
386
+ """Append an event to a session.
387
+
388
+ Args:
389
+ event_record: Event record to store.
390
+
391
+ Notes:
392
+ Uses CURRENT_TIMESTAMP for timestamp if not provided.
393
+ JSONB fields are wrapped with Jsonb() for PostgreSQL type safety.
394
+ """
395
+ content_json = event_record.get("content")
396
+ grounding_metadata_json = event_record.get("grounding_metadata")
397
+ custom_metadata_json = event_record.get("custom_metadata")
398
+
399
+ query = pg_sql.SQL("""
400
+ INSERT INTO {table} (
401
+ id, session_id, app_name, user_id, invocation_id, author, actions,
402
+ long_running_tool_ids_json, branch, timestamp, content,
403
+ grounding_metadata, custom_metadata, partial, turn_complete,
404
+ interrupted, error_code, error_message
405
+ ) VALUES (
406
+ %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s
407
+ )
408
+ """).format(table=pg_sql.Identifier(self._events_table))
409
+
410
+ async with self._config.provide_connection() as conn, conn.cursor() as cur:
411
+ await cur.execute(
412
+ query,
413
+ (
414
+ event_record["id"],
415
+ event_record["session_id"],
416
+ event_record["app_name"],
417
+ event_record["user_id"],
418
+ event_record.get("invocation_id"),
419
+ event_record.get("author"),
420
+ event_record.get("actions"),
421
+ event_record.get("long_running_tool_ids_json"),
422
+ event_record.get("branch"),
423
+ event_record["timestamp"],
424
+ Jsonb(content_json) if content_json is not None else None,
425
+ Jsonb(grounding_metadata_json) if grounding_metadata_json is not None else None,
426
+ Jsonb(custom_metadata_json) if custom_metadata_json is not None else None,
427
+ event_record.get("partial"),
428
+ event_record.get("turn_complete"),
429
+ event_record.get("interrupted"),
430
+ event_record.get("error_code"),
431
+ event_record.get("error_message"),
432
+ ),
433
+ )
434
+
435
+ async def get_events(
436
+ self, session_id: str, after_timestamp: "datetime | None" = None, limit: "int | None" = None
437
+ ) -> "list[EventRecord]":
438
+ """Get events for a session.
439
+
440
+ Args:
441
+ session_id: Session identifier.
442
+ after_timestamp: Only return events after this time.
443
+ limit: Maximum number of events to return.
444
+
445
+ Returns:
446
+ List of event records ordered by timestamp ASC.
447
+
448
+ Notes:
449
+ Uses index on (session_id, timestamp ASC).
450
+ JSONB fields are automatically deserialized by psycopg.
451
+ BYTEA actions are converted to bytes.
452
+ """
453
+ where_clauses = ["session_id = %s"]
454
+ params: list[Any] = [session_id]
455
+
456
+ if after_timestamp is not None:
457
+ where_clauses.append("timestamp > %s")
458
+ params.append(after_timestamp)
459
+
460
+ where_clause = " AND ".join(where_clauses)
461
+ if limit:
462
+ params.append(limit)
463
+
464
+ query = pg_sql.SQL(
465
+ """
466
+ SELECT id, session_id, app_name, user_id, invocation_id, author, actions,
467
+ long_running_tool_ids_json, branch, timestamp, content,
468
+ grounding_metadata, custom_metadata, partial, turn_complete,
469
+ interrupted, error_code, error_message
470
+ FROM {table}
471
+ WHERE {where_clause}
472
+ ORDER BY timestamp ASC{limit_clause}
473
+ """
474
+ ).format(
475
+ table=pg_sql.Identifier(self._events_table),
476
+ where_clause=pg_sql.SQL(where_clause), # pyright: ignore[reportArgumentType]
477
+ limit_clause=pg_sql.SQL(" LIMIT %s" if limit else ""), # pyright: ignore[reportArgumentType]
478
+ )
479
+
480
+ try:
481
+ async with self._config.provide_connection() as conn, conn.cursor() as cur:
482
+ await cur.execute(query, tuple(params))
483
+ rows = await cur.fetchall()
484
+
485
+ return [
486
+ EventRecord(
487
+ id=row["id"],
488
+ session_id=row["session_id"],
489
+ app_name=row["app_name"],
490
+ user_id=row["user_id"],
491
+ invocation_id=row["invocation_id"],
492
+ author=row["author"],
493
+ actions=bytes(row["actions"]) if row["actions"] else b"",
494
+ long_running_tool_ids_json=row["long_running_tool_ids_json"],
495
+ branch=row["branch"],
496
+ timestamp=row["timestamp"],
497
+ content=row["content"],
498
+ grounding_metadata=row["grounding_metadata"],
499
+ custom_metadata=row["custom_metadata"],
500
+ partial=row["partial"],
501
+ turn_complete=row["turn_complete"],
502
+ interrupted=row["interrupted"],
503
+ error_code=row["error_code"],
504
+ error_message=row["error_message"],
505
+ )
506
+ for row in rows
507
+ ]
508
+ except errors.UndefinedTable:
509
+ return []
510
+
511
+
512
+ class PsycopgSyncADKStore(BaseSyncADKStore["PsycopgSyncConfig"]):
513
+ """PostgreSQL synchronous ADK store using Psycopg3 driver.
514
+
515
+ Implements session and event storage for Google Agent Development Kit
516
+ using PostgreSQL via psycopg3 with synchronous execution.
517
+
518
+ Provides:
519
+ - Session state management with JSONB storage and merge operations
520
+ - Event history tracking with BYTEA-serialized actions
521
+ - Microsecond-precision timestamps with TIMESTAMPTZ
522
+ - Foreign key constraints with cascade delete
523
+ - Efficient upserts using ON CONFLICT
524
+ - GIN indexes for JSONB queries
525
+ - HOT updates with FILLFACTOR 80
526
+
527
+ Args:
528
+ config: PsycopgSyncConfig with extension_config["adk"] settings.
529
+
530
+ Example:
531
+ from sqlspec.adapters.psycopg import PsycopgSyncConfig
532
+ from sqlspec.adapters.psycopg.adk import PsycopgSyncADKStore
533
+
534
+ config = PsycopgSyncConfig(
535
+ connection_config={"conninfo": "postgresql://..."},
536
+ extension_config={
537
+ "adk": {
538
+ "session_table": "my_sessions",
539
+ "events_table": "my_events",
540
+ "owner_id_column": "tenant_id INTEGER NOT NULL REFERENCES tenants(id) ON DELETE CASCADE"
541
+ }
542
+ }
543
+ )
544
+ store = PsycopgSyncADKStore(config)
545
+ store.ensure_tables()
546
+
547
+ Notes:
548
+ - PostgreSQL JSONB type used for state (more efficient than JSON)
549
+ - Psycopg requires wrapping dicts with Jsonb() for type safety
550
+ - TIMESTAMPTZ provides timezone-aware microsecond precision
551
+ - State merging uses `state || $1::jsonb` operator for efficiency
552
+ - BYTEA for pre-serialized actions from Google ADK
553
+ - GIN index on state for JSONB queries (partial index)
554
+ - FILLFACTOR 80 leaves space for HOT updates
555
+ - Parameter style: $1, $2, $3 (PostgreSQL numeric placeholders)
556
+ - Configuration is read from config.extension_config["adk"]
557
+ """
558
+
559
+ __slots__ = ()
560
+
561
+ def __init__(self, config: "PsycopgSyncConfig") -> None:
562
+ """Initialize Psycopg synchronous ADK store.
563
+
564
+ Args:
565
+ config: PsycopgSyncConfig instance.
566
+
567
+ Notes:
568
+ Configuration is read from config.extension_config["adk"]:
569
+ - session_table: Sessions table name (default: "adk_sessions")
570
+ - events_table: Events table name (default: "adk_events")
571
+ - owner_id_column: Optional owner FK column DDL (default: None)
572
+ """
573
+ super().__init__(config)
574
+
575
+ def _get_create_sessions_table_sql(self) -> str:
576
+ """Get PostgreSQL CREATE TABLE SQL for sessions.
577
+
578
+ Returns:
579
+ SQL statement to create adk_sessions table with indexes.
580
+
581
+ Notes:
582
+ - VARCHAR(128) for IDs and names (sufficient for UUIDs and app names)
583
+ - JSONB type for state storage with default empty object
584
+ - TIMESTAMPTZ with microsecond precision
585
+ - FILLFACTOR 80 for HOT updates (reduces table bloat)
586
+ - Composite index on (app_name, user_id) for listing
587
+ - Index on update_time DESC for recent session queries
588
+ - Partial GIN index on state for JSONB queries (only non-empty)
589
+ - Optional owner ID column for multi-tenancy or user references
590
+ """
591
+ owner_id_line = ""
592
+ if self._owner_id_column_ddl:
593
+ owner_id_line = f",\n {self._owner_id_column_ddl}"
594
+
595
+ return f"""
596
+ CREATE TABLE IF NOT EXISTS {self._session_table} (
597
+ id VARCHAR(128) PRIMARY KEY,
598
+ app_name VARCHAR(128) NOT NULL,
599
+ user_id VARCHAR(128) NOT NULL{owner_id_line},
600
+ state JSONB NOT NULL DEFAULT '{{}}'::jsonb,
601
+ create_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
602
+ update_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
603
+ ) WITH (fillfactor = 80);
604
+
605
+ CREATE INDEX IF NOT EXISTS idx_{self._session_table}_app_user
606
+ ON {self._session_table}(app_name, user_id);
607
+
608
+ CREATE INDEX IF NOT EXISTS idx_{self._session_table}_update_time
609
+ ON {self._session_table}(update_time DESC);
610
+
611
+ CREATE INDEX IF NOT EXISTS idx_{self._session_table}_state
612
+ ON {self._session_table} USING GIN (state)
613
+ WHERE state != '{{}}'::jsonb;
614
+ """
615
+
616
+ def _get_create_events_table_sql(self) -> str:
617
+ """Get PostgreSQL CREATE TABLE SQL for events.
618
+
619
+ Returns:
620
+ SQL statement to create adk_events table with indexes.
621
+
622
+ Notes:
623
+ - VARCHAR sizes: id(128), session_id(128), invocation_id(256), author(256),
624
+ branch(256), error_code(256), error_message(1024)
625
+ - BYTEA for pickled actions (no size limit)
626
+ - JSONB for content, grounding_metadata, custom_metadata, long_running_tool_ids_json
627
+ - BOOLEAN for partial, turn_complete, interrupted
628
+ - Foreign key to sessions with CASCADE delete
629
+ - Index on (session_id, timestamp ASC) for ordered event retrieval
630
+ """
631
+ return f"""
632
+ CREATE TABLE IF NOT EXISTS {self._events_table} (
633
+ id VARCHAR(128) PRIMARY KEY,
634
+ session_id VARCHAR(128) NOT NULL,
635
+ app_name VARCHAR(128) NOT NULL,
636
+ user_id VARCHAR(128) NOT NULL,
637
+ invocation_id VARCHAR(256),
638
+ author VARCHAR(256),
639
+ actions BYTEA,
640
+ long_running_tool_ids_json JSONB,
641
+ branch VARCHAR(256),
642
+ timestamp TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
643
+ content JSONB,
644
+ grounding_metadata JSONB,
645
+ custom_metadata JSONB,
646
+ partial BOOLEAN,
647
+ turn_complete BOOLEAN,
648
+ interrupted BOOLEAN,
649
+ error_code VARCHAR(256),
650
+ error_message VARCHAR(1024),
651
+ FOREIGN KEY (session_id) REFERENCES {self._session_table}(id) ON DELETE CASCADE
652
+ );
653
+
654
+ CREATE INDEX IF NOT EXISTS idx_{self._events_table}_session
655
+ ON {self._events_table}(session_id, timestamp ASC);
656
+ """
657
+
658
+ def _get_drop_tables_sql(self) -> "list[str]":
659
+ """Get PostgreSQL DROP TABLE SQL statements.
660
+
661
+ Returns:
662
+ List of SQL statements to drop tables and indexes.
663
+
664
+ Notes:
665
+ Order matters: drop events table (child) before sessions (parent).
666
+ PostgreSQL automatically drops indexes when dropping tables.
667
+ """
668
+ return [f"DROP TABLE IF EXISTS {self._events_table}", f"DROP TABLE IF EXISTS {self._session_table}"]
669
+
670
+ def create_tables(self) -> None:
671
+ """Create both sessions and events tables if they don't exist."""
672
+ with self._config.provide_session() as driver:
673
+ driver.execute_script(self._get_create_sessions_table_sql())
674
+ driver.execute_script(self._get_create_events_table_sql())
675
+
676
+ def create_session(
677
+ self, session_id: str, app_name: str, user_id: str, state: "dict[str, Any]", owner_id: "Any | None" = None
678
+ ) -> SessionRecord:
679
+ """Create a new session.
680
+
681
+ Args:
682
+ session_id: Unique session identifier.
683
+ app_name: Application name.
684
+ user_id: User identifier.
685
+ state: Initial session state.
686
+ owner_id: Optional owner ID value for owner_id_column (if configured).
687
+
688
+ Returns:
689
+ Created session record.
690
+
691
+ Notes:
692
+ Uses CURRENT_TIMESTAMP for create_time and update_time.
693
+ State is wrapped with Jsonb() for PostgreSQL type safety.
694
+ If owner_id_column is configured, owner_id value must be provided.
695
+ """
696
+ params: tuple[Any, ...]
697
+ if self._owner_id_column_name:
698
+ query = pg_sql.SQL("""
699
+ INSERT INTO {table} (id, app_name, user_id, {owner_id_col}, state, create_time, update_time)
700
+ VALUES (%s, %s, %s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
701
+ """).format(
702
+ table=pg_sql.Identifier(self._session_table), owner_id_col=pg_sql.Identifier(self._owner_id_column_name)
703
+ )
704
+ params = (session_id, app_name, user_id, owner_id, Jsonb(state))
705
+ else:
706
+ query = pg_sql.SQL("""
707
+ INSERT INTO {table} (id, app_name, user_id, state, create_time, update_time)
708
+ VALUES (%s, %s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
709
+ """).format(table=pg_sql.Identifier(self._session_table))
710
+ params = (session_id, app_name, user_id, Jsonb(state))
711
+
712
+ with self._config.provide_connection() as conn, conn.cursor() as cur:
713
+ cur.execute(query, params)
714
+
715
+ return self.get_session(session_id) # type: ignore[return-value]
716
+
717
+ def get_session(self, session_id: str) -> "SessionRecord | None":
718
+ """Get session by ID.
719
+
720
+ Args:
721
+ session_id: Session identifier.
722
+
723
+ Returns:
724
+ Session record or None if not found.
725
+
726
+ Notes:
727
+ PostgreSQL returns datetime objects for TIMESTAMPTZ columns.
728
+ JSONB is automatically deserialized by psycopg to Python dict.
729
+ """
730
+ query = pg_sql.SQL("""
731
+ SELECT id, app_name, user_id, state, create_time, update_time
732
+ FROM {table}
733
+ WHERE id = %s
734
+ """).format(table=pg_sql.Identifier(self._session_table))
735
+
736
+ try:
737
+ with self._config.provide_connection() as conn, conn.cursor() as cur:
738
+ cur.execute(query, (session_id,))
739
+ row = cur.fetchone()
740
+
741
+ if row is None:
742
+ return None
743
+
744
+ return SessionRecord(
745
+ id=row["id"],
746
+ app_name=row["app_name"],
747
+ user_id=row["user_id"],
748
+ state=row["state"],
749
+ create_time=row["create_time"],
750
+ update_time=row["update_time"],
751
+ )
752
+ except errors.UndefinedTable:
753
+ return None
754
+
755
+ def update_session_state(self, session_id: str, state: "dict[str, Any]") -> None:
756
+ """Update session state.
757
+
758
+ Args:
759
+ session_id: Session identifier.
760
+ state: New state dictionary (replaces existing state).
761
+
762
+ Notes:
763
+ This replaces the entire state dictionary.
764
+ Uses CURRENT_TIMESTAMP for update_time.
765
+ State is wrapped with Jsonb() for PostgreSQL type safety.
766
+ """
767
+ query = pg_sql.SQL("""
768
+ UPDATE {table}
769
+ SET state = %s, update_time = CURRENT_TIMESTAMP
770
+ WHERE id = %s
771
+ """).format(table=pg_sql.Identifier(self._session_table))
772
+
773
+ with self._config.provide_connection() as conn, conn.cursor() as cur:
774
+ cur.execute(query, (Jsonb(state), session_id))
775
+
776
+ def delete_session(self, session_id: str) -> None:
777
+ """Delete session and all associated events (cascade).
778
+
779
+ Args:
780
+ session_id: Session identifier.
781
+
782
+ Notes:
783
+ Foreign key constraint ensures events are cascade-deleted.
784
+ """
785
+ query = pg_sql.SQL("DELETE FROM {table} WHERE id = %s").format(table=pg_sql.Identifier(self._session_table))
786
+
787
+ with self._config.provide_connection() as conn, conn.cursor() as cur:
788
+ cur.execute(query, (session_id,))
789
+
790
+ def list_sessions(self, app_name: str, user_id: str | None = None) -> "list[SessionRecord]":
791
+ """List sessions for an app, optionally filtered by user.
792
+
793
+ Args:
794
+ app_name: Application name.
795
+ user_id: User identifier. If None, lists all sessions for the app.
796
+
797
+ Returns:
798
+ List of session records ordered by update_time DESC.
799
+
800
+ Notes:
801
+ Uses composite index on (app_name, user_id) when user_id is provided.
802
+ """
803
+ if user_id is None:
804
+ query = pg_sql.SQL("""
805
+ SELECT id, app_name, user_id, state, create_time, update_time
806
+ FROM {table}
807
+ WHERE app_name = %s
808
+ ORDER BY update_time DESC
809
+ """).format(table=pg_sql.Identifier(self._session_table))
810
+ params: tuple[str, ...] = (app_name,)
811
+ else:
812
+ query = pg_sql.SQL("""
813
+ SELECT id, app_name, user_id, state, create_time, update_time
814
+ FROM {table}
815
+ WHERE app_name = %s AND user_id = %s
816
+ ORDER BY update_time DESC
817
+ """).format(table=pg_sql.Identifier(self._session_table))
818
+ params = (app_name, user_id)
819
+
820
+ try:
821
+ with self._config.provide_connection() as conn, conn.cursor() as cur:
822
+ cur.execute(query, params)
823
+ rows = cur.fetchall()
824
+
825
+ return [
826
+ SessionRecord(
827
+ id=row["id"],
828
+ app_name=row["app_name"],
829
+ user_id=row["user_id"],
830
+ state=row["state"],
831
+ create_time=row["create_time"],
832
+ update_time=row["update_time"],
833
+ )
834
+ for row in rows
835
+ ]
836
+ except errors.UndefinedTable:
837
+ return []
838
+
839
+ def create_event(
840
+ self,
841
+ event_id: str,
842
+ session_id: str,
843
+ app_name: str,
844
+ user_id: str,
845
+ author: "str | None" = None,
846
+ actions: "bytes | None" = None,
847
+ content: "dict[str, Any] | None" = None,
848
+ **kwargs: Any,
849
+ ) -> EventRecord:
850
+ """Create a new event.
851
+
852
+ Args:
853
+ event_id: Unique event identifier.
854
+ session_id: Session identifier.
855
+ app_name: Application name.
856
+ user_id: User identifier.
857
+ author: Event author (user/assistant/system).
858
+ actions: Pickled actions object.
859
+ content: Event content (JSONB).
860
+ **kwargs: Additional optional fields (invocation_id, branch, timestamp,
861
+ grounding_metadata, custom_metadata, partial, turn_complete,
862
+ interrupted, error_code, error_message, long_running_tool_ids_json).
863
+
864
+ Returns:
865
+ Created event record.
866
+
867
+ Notes:
868
+ Uses CURRENT_TIMESTAMP for timestamp if not provided in kwargs.
869
+ JSONB fields are wrapped with Jsonb() for PostgreSQL type safety.
870
+ """
871
+ content_json = Jsonb(content) if content is not None else None
872
+ grounding_metadata = kwargs.get("grounding_metadata")
873
+ grounding_metadata_json = Jsonb(grounding_metadata) if grounding_metadata is not None else None
874
+ custom_metadata = kwargs.get("custom_metadata")
875
+ custom_metadata_json = Jsonb(custom_metadata) if custom_metadata is not None else None
876
+
877
+ query = pg_sql.SQL("""
878
+ INSERT INTO {table} (
879
+ id, session_id, app_name, user_id, invocation_id, author, actions,
880
+ long_running_tool_ids_json, branch, timestamp, content,
881
+ grounding_metadata, custom_metadata, partial, turn_complete,
882
+ interrupted, error_code, error_message
883
+ ) VALUES (
884
+ %s, %s, %s, %s, %s, %s, %s, %s, %s, COALESCE(%s, CURRENT_TIMESTAMP), %s, %s, %s, %s, %s, %s, %s, %s
885
+ )
886
+ RETURNING id, session_id, app_name, user_id, invocation_id, author, actions,
887
+ long_running_tool_ids_json, branch, timestamp, content,
888
+ grounding_metadata, custom_metadata, partial, turn_complete,
889
+ interrupted, error_code, error_message
890
+ """).format(table=pg_sql.Identifier(self._events_table))
891
+
892
+ with self._config.provide_connection() as conn, conn.cursor() as cur:
893
+ cur.execute(
894
+ query,
895
+ (
896
+ event_id,
897
+ session_id,
898
+ app_name,
899
+ user_id,
900
+ kwargs.get("invocation_id"),
901
+ author,
902
+ actions,
903
+ kwargs.get("long_running_tool_ids_json"),
904
+ kwargs.get("branch"),
905
+ kwargs.get("timestamp"),
906
+ content_json,
907
+ grounding_metadata_json,
908
+ custom_metadata_json,
909
+ kwargs.get("partial"),
910
+ kwargs.get("turn_complete"),
911
+ kwargs.get("interrupted"),
912
+ kwargs.get("error_code"),
913
+ kwargs.get("error_message"),
914
+ ),
915
+ )
916
+ row = cur.fetchone()
917
+
918
+ if row is None:
919
+ msg = f"Failed to create event {event_id}"
920
+ raise RuntimeError(msg)
921
+
922
+ return EventRecord(
923
+ id=row["id"],
924
+ session_id=row["session_id"],
925
+ app_name=row["app_name"],
926
+ user_id=row["user_id"],
927
+ invocation_id=row["invocation_id"],
928
+ author=row["author"],
929
+ actions=bytes(row["actions"]) if row["actions"] else b"",
930
+ long_running_tool_ids_json=row["long_running_tool_ids_json"],
931
+ branch=row["branch"],
932
+ timestamp=row["timestamp"],
933
+ content=row["content"],
934
+ grounding_metadata=row["grounding_metadata"],
935
+ custom_metadata=row["custom_metadata"],
936
+ partial=row["partial"],
937
+ turn_complete=row["turn_complete"],
938
+ interrupted=row["interrupted"],
939
+ error_code=row["error_code"],
940
+ error_message=row["error_message"],
941
+ )
942
+
943
+ def list_events(self, session_id: str) -> "list[EventRecord]":
944
+ """List events for a session ordered by timestamp.
945
+
946
+ Args:
947
+ session_id: Session identifier.
948
+
949
+ Returns:
950
+ List of event records ordered by timestamp ASC.
951
+
952
+ Notes:
953
+ Uses index on (session_id, timestamp ASC).
954
+ JSONB fields are automatically deserialized by psycopg.
955
+ BYTEA actions are converted to bytes.
956
+ """
957
+ query = pg_sql.SQL("""
958
+ SELECT id, session_id, app_name, user_id, invocation_id, author, actions,
959
+ long_running_tool_ids_json, branch, timestamp, content,
960
+ grounding_metadata, custom_metadata, partial, turn_complete,
961
+ interrupted, error_code, error_message
962
+ FROM {table}
963
+ WHERE session_id = %s
964
+ ORDER BY timestamp ASC
965
+ """).format(table=pg_sql.Identifier(self._events_table))
966
+
967
+ try:
968
+ with self._config.provide_connection() as conn, conn.cursor() as cur:
969
+ cur.execute(query, (session_id,))
970
+ rows = cur.fetchall()
971
+
972
+ return [
973
+ EventRecord(
974
+ id=row["id"],
975
+ session_id=row["session_id"],
976
+ app_name=row["app_name"],
977
+ user_id=row["user_id"],
978
+ invocation_id=row["invocation_id"],
979
+ author=row["author"],
980
+ actions=bytes(row["actions"]) if row["actions"] else b"",
981
+ long_running_tool_ids_json=row["long_running_tool_ids_json"],
982
+ branch=row["branch"],
983
+ timestamp=row["timestamp"],
984
+ content=row["content"],
985
+ grounding_metadata=row["grounding_metadata"],
986
+ custom_metadata=row["custom_metadata"],
987
+ partial=row["partial"],
988
+ turn_complete=row["turn_complete"],
989
+ interrupted=row["interrupted"],
990
+ error_code=row["error_code"],
991
+ error_message=row["error_message"],
992
+ )
993
+ for row in rows
994
+ ]
995
+ except errors.UndefinedTable:
996
+ return []
997
+
998
+
999
+ class PsycopgAsyncADKMemoryStore(BaseAsyncADKMemoryStore["PsycopgAsyncConfig"]):
1000
+ """PostgreSQL ADK memory store using Psycopg3 async driver."""
1001
+
1002
+ __slots__ = ()
1003
+
1004
+ def __init__(self, config: "PsycopgAsyncConfig") -> None:
1005
+ """Initialize Psycopg async memory store."""
1006
+ super().__init__(config)
1007
+
1008
+ async def _get_create_memory_table_sql(self) -> str:
1009
+ """Get PostgreSQL CREATE TABLE SQL for memory entries."""
1010
+ owner_id_line = ""
1011
+ if self._owner_id_column_ddl:
1012
+ owner_id_line = f",\n {self._owner_id_column_ddl}"
1013
+
1014
+ fts_index = ""
1015
+ if self._use_fts:
1016
+ fts_index = f"""
1017
+ CREATE INDEX IF NOT EXISTS idx_{self._memory_table}_fts
1018
+ ON {self._memory_table} USING GIN (to_tsvector('english', content_text));
1019
+ """
1020
+
1021
+ return f"""
1022
+ CREATE TABLE IF NOT EXISTS {self._memory_table} (
1023
+ id VARCHAR(128) PRIMARY KEY,
1024
+ session_id VARCHAR(128) NOT NULL,
1025
+ app_name VARCHAR(128) NOT NULL,
1026
+ user_id VARCHAR(128) NOT NULL,
1027
+ event_id VARCHAR(128) NOT NULL UNIQUE,
1028
+ author VARCHAR(256){owner_id_line},
1029
+ timestamp TIMESTAMPTZ NOT NULL,
1030
+ content_json JSONB NOT NULL,
1031
+ content_text TEXT NOT NULL,
1032
+ metadata_json JSONB,
1033
+ inserted_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
1034
+ );
1035
+
1036
+ CREATE INDEX IF NOT EXISTS idx_{self._memory_table}_app_user_time
1037
+ ON {self._memory_table}(app_name, user_id, timestamp DESC);
1038
+
1039
+ CREATE INDEX IF NOT EXISTS idx_{self._memory_table}_session
1040
+ ON {self._memory_table}(session_id);
1041
+ {fts_index}
1042
+ """
1043
+
1044
+ def _get_drop_memory_table_sql(self) -> "list[str]":
1045
+ """Get PostgreSQL DROP TABLE SQL statements."""
1046
+ return [f"DROP TABLE IF EXISTS {self._memory_table}"]
1047
+
1048
+ async def create_tables(self) -> None:
1049
+ """Create the memory table and indexes if they don't exist."""
1050
+ if not self._enabled:
1051
+ return
1052
+
1053
+ async with self._config.provide_session() as driver:
1054
+ await driver.execute_script(await self._get_create_memory_table_sql())
1055
+
1056
+ async def insert_memory_entries(self, entries: "list[MemoryRecord]", owner_id: "object | None" = None) -> int:
1057
+ """Bulk insert memory entries with deduplication."""
1058
+ if not self._enabled:
1059
+ msg = "Memory store is disabled"
1060
+ raise RuntimeError(msg)
1061
+
1062
+ if not entries:
1063
+ return 0
1064
+
1065
+ inserted_count = 0
1066
+ if self._owner_id_column_name:
1067
+ query = pg_sql.SQL("""
1068
+ INSERT INTO {table} (
1069
+ id, session_id, app_name, user_id, event_id, author,
1070
+ {owner_id_col}, timestamp, content_json, content_text,
1071
+ metadata_json, inserted_at
1072
+ ) VALUES (
1073
+ %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s
1074
+ )
1075
+ ON CONFLICT (event_id) DO NOTHING
1076
+ """).format(
1077
+ table=pg_sql.Identifier(self._memory_table), owner_id_col=pg_sql.Identifier(self._owner_id_column_name)
1078
+ )
1079
+ else:
1080
+ query = pg_sql.SQL("""
1081
+ INSERT INTO {table} (
1082
+ id, session_id, app_name, user_id, event_id, author,
1083
+ timestamp, content_json, content_text, metadata_json, inserted_at
1084
+ ) VALUES (
1085
+ %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s
1086
+ )
1087
+ ON CONFLICT (event_id) DO NOTHING
1088
+ """).format(table=pg_sql.Identifier(self._memory_table))
1089
+
1090
+ async with self._config.provide_connection() as conn, conn.cursor() as cur:
1091
+ for entry in entries:
1092
+ if self._owner_id_column_name:
1093
+ await cur.execute(query, _build_insert_params_with_owner(entry, owner_id))
1094
+ else:
1095
+ await cur.execute(query, _build_insert_params(entry))
1096
+ if cur.rowcount and cur.rowcount > 0:
1097
+ inserted_count += cur.rowcount
1098
+
1099
+ return inserted_count
1100
+
1101
+ async def search_entries(
1102
+ self, query: str, app_name: str, user_id: str, limit: "int | None" = None
1103
+ ) -> "list[MemoryRecord]":
1104
+ """Search memory entries by text query."""
1105
+ if not self._enabled:
1106
+ msg = "Memory store is disabled"
1107
+ raise RuntimeError(msg)
1108
+
1109
+ effective_limit = limit if limit is not None else self._max_results
1110
+
1111
+ try:
1112
+ if self._use_fts:
1113
+ try:
1114
+ return await self._search_entries_fts(query, app_name, user_id, effective_limit)
1115
+ except Exception as exc: # pragma: no cover - defensive fallback
1116
+ logger.warning("FTS search failed; falling back to simple search: %s", exc)
1117
+ return await self._search_entries_simple(query, app_name, user_id, effective_limit)
1118
+ except errors.UndefinedTable:
1119
+ return []
1120
+
1121
+ async def _search_entries_fts(self, query: str, app_name: str, user_id: str, limit: int) -> "list[MemoryRecord]":
1122
+ sql = pg_sql.SQL(
1123
+ """
1124
+ SELECT id, session_id, app_name, user_id, event_id, author,
1125
+ timestamp, content_json, content_text, metadata_json, inserted_at,
1126
+ ts_rank(to_tsvector('english', content_text), plainto_tsquery('english', %s)) as rank
1127
+ FROM {table}
1128
+ WHERE app_name = %s
1129
+ AND user_id = %s
1130
+ AND to_tsvector('english', content_text) @@ plainto_tsquery('english', %s)
1131
+ ORDER BY rank DESC, timestamp DESC
1132
+ LIMIT %s
1133
+ """
1134
+ ).format(table=pg_sql.Identifier(self._memory_table))
1135
+ params: tuple[str, str, str, str, int] = (query, app_name, user_id, query, limit)
1136
+ async with self._config.provide_connection() as conn, conn.cursor() as cur:
1137
+ await cur.execute(sql, params)
1138
+ rows = await cur.fetchall()
1139
+ return _rows_to_records(rows)
1140
+
1141
+ async def _search_entries_simple(self, query: str, app_name: str, user_id: str, limit: int) -> "list[MemoryRecord]":
1142
+ sql = pg_sql.SQL(
1143
+ """
1144
+ SELECT id, session_id, app_name, user_id, event_id, author,
1145
+ timestamp, content_json, content_text, metadata_json, inserted_at
1146
+ FROM {table}
1147
+ WHERE app_name = %s
1148
+ AND user_id = %s
1149
+ AND content_text ILIKE %s
1150
+ ORDER BY timestamp DESC
1151
+ LIMIT %s
1152
+ """
1153
+ ).format(table=pg_sql.Identifier(self._memory_table))
1154
+ pattern = f"%{query}%"
1155
+ params: tuple[str, str, str, int] = (app_name, user_id, pattern, limit)
1156
+ async with self._config.provide_connection() as conn, conn.cursor() as cur:
1157
+ await cur.execute(sql, params)
1158
+ rows = await cur.fetchall()
1159
+ return _rows_to_records(rows)
1160
+
1161
+ async def delete_entries_by_session(self, session_id: str) -> int:
1162
+ """Delete all memory entries for a specific session."""
1163
+ sql = pg_sql.SQL("DELETE FROM {table} WHERE session_id = %s").format(
1164
+ table=pg_sql.Identifier(self._memory_table)
1165
+ )
1166
+
1167
+ async with self._config.provide_connection() as conn, conn.cursor() as cur:
1168
+ await cur.execute(sql, (session_id,))
1169
+ return cur.rowcount if cur.rowcount and cur.rowcount > 0 else 0
1170
+
1171
+ async def delete_entries_older_than(self, days: int) -> int:
1172
+ """Delete memory entries older than specified days."""
1173
+ sql = pg_sql.SQL(
1174
+ """
1175
+ DELETE FROM {table}
1176
+ WHERE inserted_at < CURRENT_TIMESTAMP - {interval}::interval
1177
+ """
1178
+ ).format(table=pg_sql.Identifier(self._memory_table), interval=pg_sql.Literal(f"{days} days"))
1179
+
1180
+ async with self._config.provide_connection() as conn, conn.cursor() as cur:
1181
+ await cur.execute(sql)
1182
+ return cur.rowcount if cur.rowcount and cur.rowcount > 0 else 0
1183
+
1184
+
1185
+ class PsycopgSyncADKMemoryStore(BaseSyncADKMemoryStore["PsycopgSyncConfig"]):
1186
+ """PostgreSQL ADK memory store using Psycopg3 sync driver."""
1187
+
1188
+ __slots__ = ()
1189
+
1190
+ def __init__(self, config: "PsycopgSyncConfig") -> None:
1191
+ """Initialize Psycopg sync memory store."""
1192
+ super().__init__(config)
1193
+
1194
+ def _get_create_memory_table_sql(self) -> str:
1195
+ """Get PostgreSQL CREATE TABLE SQL for memory entries."""
1196
+ owner_id_line = ""
1197
+ if self._owner_id_column_ddl:
1198
+ owner_id_line = f",\n {self._owner_id_column_ddl}"
1199
+
1200
+ fts_index = ""
1201
+ if self._use_fts:
1202
+ fts_index = f"""
1203
+ CREATE INDEX IF NOT EXISTS idx_{self._memory_table}_fts
1204
+ ON {self._memory_table} USING GIN (to_tsvector('english', content_text));
1205
+ """
1206
+
1207
+ return f"""
1208
+ CREATE TABLE IF NOT EXISTS {self._memory_table} (
1209
+ id VARCHAR(128) PRIMARY KEY,
1210
+ session_id VARCHAR(128) NOT NULL,
1211
+ app_name VARCHAR(128) NOT NULL,
1212
+ user_id VARCHAR(128) NOT NULL,
1213
+ event_id VARCHAR(128) NOT NULL UNIQUE,
1214
+ author VARCHAR(256){owner_id_line},
1215
+ timestamp TIMESTAMPTZ NOT NULL,
1216
+ content_json JSONB NOT NULL,
1217
+ content_text TEXT NOT NULL,
1218
+ metadata_json JSONB,
1219
+ inserted_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
1220
+ );
1221
+
1222
+ CREATE INDEX IF NOT EXISTS idx_{self._memory_table}_app_user_time
1223
+ ON {self._memory_table}(app_name, user_id, timestamp DESC);
1224
+
1225
+ CREATE INDEX IF NOT EXISTS idx_{self._memory_table}_session
1226
+ ON {self._memory_table}(session_id);
1227
+ {fts_index}
1228
+ """
1229
+
1230
+ def _get_drop_memory_table_sql(self) -> "list[str]":
1231
+ """Get PostgreSQL DROP TABLE SQL statements."""
1232
+ return [f"DROP TABLE IF EXISTS {self._memory_table}"]
1233
+
1234
+ def create_tables(self) -> None:
1235
+ """Create the memory table and indexes if they don't exist."""
1236
+ if not self._enabled:
1237
+ return
1238
+
1239
+ with self._config.provide_session() as driver:
1240
+ driver.execute_script(self._get_create_memory_table_sql())
1241
+
1242
+ def insert_memory_entries(self, entries: "list[MemoryRecord]", owner_id: "object | None" = None) -> int:
1243
+ """Bulk insert memory entries with deduplication."""
1244
+ if not self._enabled:
1245
+ msg = "Memory store is disabled"
1246
+ raise RuntimeError(msg)
1247
+
1248
+ if not entries:
1249
+ return 0
1250
+
1251
+ inserted_count = 0
1252
+ if self._owner_id_column_name:
1253
+ query = pg_sql.SQL("""
1254
+ INSERT INTO {table} (
1255
+ id, session_id, app_name, user_id, event_id, author,
1256
+ {owner_id_col}, timestamp, content_json, content_text,
1257
+ metadata_json, inserted_at
1258
+ ) VALUES (
1259
+ %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s
1260
+ )
1261
+ ON CONFLICT (event_id) DO NOTHING
1262
+ """).format(
1263
+ table=pg_sql.Identifier(self._memory_table), owner_id_col=pg_sql.Identifier(self._owner_id_column_name)
1264
+ )
1265
+ else:
1266
+ query = pg_sql.SQL("""
1267
+ INSERT INTO {table} (
1268
+ id, session_id, app_name, user_id, event_id, author,
1269
+ timestamp, content_json, content_text, metadata_json, inserted_at
1270
+ ) VALUES (
1271
+ %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s
1272
+ )
1273
+ ON CONFLICT (event_id) DO NOTHING
1274
+ """).format(table=pg_sql.Identifier(self._memory_table))
1275
+
1276
+ with self._config.provide_connection() as conn, conn.cursor() as cur:
1277
+ for entry in entries:
1278
+ if self._owner_id_column_name:
1279
+ cur.execute(query, _build_insert_params_with_owner(entry, owner_id))
1280
+ else:
1281
+ cur.execute(query, _build_insert_params(entry))
1282
+ if cur.rowcount and cur.rowcount > 0:
1283
+ inserted_count += cur.rowcount
1284
+
1285
+ return inserted_count
1286
+
1287
+ def search_entries(
1288
+ self, query: str, app_name: str, user_id: str, limit: "int | None" = None
1289
+ ) -> "list[MemoryRecord]":
1290
+ """Search memory entries by text query."""
1291
+ if not self._enabled:
1292
+ msg = "Memory store is disabled"
1293
+ raise RuntimeError(msg)
1294
+
1295
+ effective_limit = limit if limit is not None else self._max_results
1296
+
1297
+ try:
1298
+ if self._use_fts:
1299
+ try:
1300
+ return self._search_entries_fts(query, app_name, user_id, effective_limit)
1301
+ except Exception as exc: # pragma: no cover - defensive fallback
1302
+ logger.warning("FTS search failed; falling back to simple search: %s", exc)
1303
+ return self._search_entries_simple(query, app_name, user_id, effective_limit)
1304
+ except errors.UndefinedTable:
1305
+ return []
1306
+
1307
+ def _search_entries_fts(self, query: str, app_name: str, user_id: str, limit: int) -> "list[MemoryRecord]":
1308
+ sql = pg_sql.SQL(
1309
+ """
1310
+ SELECT id, session_id, app_name, user_id, event_id, author,
1311
+ timestamp, content_json, content_text, metadata_json, inserted_at,
1312
+ ts_rank(to_tsvector('english', content_text), plainto_tsquery('english', %s)) as rank
1313
+ FROM {table}
1314
+ WHERE app_name = %s
1315
+ AND user_id = %s
1316
+ AND to_tsvector('english', content_text) @@ plainto_tsquery('english', %s)
1317
+ ORDER BY rank DESC, timestamp DESC
1318
+ LIMIT %s
1319
+ """
1320
+ ).format(table=pg_sql.Identifier(self._memory_table))
1321
+ params: tuple[str, str, str, str, int] = (query, app_name, user_id, query, limit)
1322
+ with self._config.provide_connection() as conn, conn.cursor() as cur:
1323
+ cur.execute(sql, params)
1324
+ rows = cur.fetchall()
1325
+ return _rows_to_records(rows)
1326
+
1327
+ def _search_entries_simple(self, query: str, app_name: str, user_id: str, limit: int) -> "list[MemoryRecord]":
1328
+ sql = pg_sql.SQL(
1329
+ """
1330
+ SELECT id, session_id, app_name, user_id, event_id, author,
1331
+ timestamp, content_json, content_text, metadata_json, inserted_at
1332
+ FROM {table}
1333
+ WHERE app_name = %s
1334
+ AND user_id = %s
1335
+ AND content_text ILIKE %s
1336
+ ORDER BY timestamp DESC
1337
+ LIMIT %s
1338
+ """
1339
+ ).format(table=pg_sql.Identifier(self._memory_table))
1340
+ pattern = f"%{query}%"
1341
+ params: tuple[str, str, str, int] = (app_name, user_id, pattern, limit)
1342
+ with self._config.provide_connection() as conn, conn.cursor() as cur:
1343
+ cur.execute(sql, params)
1344
+ rows = cur.fetchall()
1345
+ return _rows_to_records(rows)
1346
+
1347
+ def delete_entries_by_session(self, session_id: str) -> int:
1348
+ """Delete all memory entries for a specific session."""
1349
+ sql = pg_sql.SQL("DELETE FROM {table} WHERE session_id = %s").format(
1350
+ table=pg_sql.Identifier(self._memory_table)
1351
+ )
1352
+
1353
+ with self._config.provide_connection() as conn, conn.cursor() as cur:
1354
+ cur.execute(sql, (session_id,))
1355
+ return cur.rowcount if cur.rowcount and cur.rowcount > 0 else 0
1356
+
1357
+ def delete_entries_older_than(self, days: int) -> int:
1358
+ """Delete memory entries older than specified days."""
1359
+ sql = pg_sql.SQL(
1360
+ """
1361
+ DELETE FROM {table}
1362
+ WHERE inserted_at < CURRENT_TIMESTAMP - {interval}::interval
1363
+ """
1364
+ ).format(table=pg_sql.Identifier(self._memory_table), interval=pg_sql.Literal(f"{days} days"))
1365
+
1366
+ with self._config.provide_connection() as conn, conn.cursor() as cur:
1367
+ cur.execute(sql)
1368
+ return cur.rowcount if cur.rowcount and cur.rowcount > 0 else 0
1369
+
1370
+
1371
+ def _rows_to_records(rows: "list[Any]") -> "list[MemoryRecord]":
1372
+ return [
1373
+ {
1374
+ "id": row["id"],
1375
+ "session_id": row["session_id"],
1376
+ "app_name": row["app_name"],
1377
+ "user_id": row["user_id"],
1378
+ "event_id": row["event_id"],
1379
+ "author": row["author"],
1380
+ "timestamp": row["timestamp"],
1381
+ "content_json": row["content_json"],
1382
+ "content_text": row["content_text"],
1383
+ "metadata_json": row["metadata_json"],
1384
+ "inserted_at": row["inserted_at"],
1385
+ }
1386
+ for row in rows
1387
+ ]