google-evalbench 1.7.1__py3-none-any.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 (189) hide show
  1. evalbench/__init__.py +12 -0
  2. evalbench/client/eval_client.py +149 -0
  3. evalbench/databases/__init__.py +42 -0
  4. evalbench/databases/alloydb.py +49 -0
  5. evalbench/databases/alloydb_omni.py +43 -0
  6. evalbench/databases/bigquery.py +297 -0
  7. evalbench/databases/bigtable.py +164 -0
  8. evalbench/databases/db.py +270 -0
  9. evalbench/databases/emulator_manager.py +133 -0
  10. evalbench/databases/mongodb.py +340 -0
  11. evalbench/databases/mysql.py +374 -0
  12. evalbench/databases/postgres.py +336 -0
  13. evalbench/databases/spanner.py +552 -0
  14. evalbench/databases/sqlite.py +297 -0
  15. evalbench/databases/sqlserver.py +320 -0
  16. evalbench/databases/util.py +162 -0
  17. evalbench/dataset/__init__.py +0 -0
  18. evalbench/dataset/cortadoinput.py +61 -0
  19. evalbench/dataset/dataset.py +326 -0
  20. evalbench/dataset/evalgeminicliinput.py +60 -0
  21. evalbench/dataset/evalinput.py +181 -0
  22. evalbench/dataset/evalinteractinput.py +123 -0
  23. evalbench/dataset/evalinteractoutput.py +9 -0
  24. evalbench/dataset/evaloutput.py +9 -0
  25. evalbench/eval_server.py +117 -0
  26. evalbench/eval_service.py +502 -0
  27. evalbench/evalbench.py +217 -0
  28. evalbench/evalproto/eval_config_pb2.py +39 -0
  29. evalbench/evalproto/eval_config_pb2.pyi +23 -0
  30. evalbench/evalproto/eval_config_pb2_grpc.py +24 -0
  31. evalbench/evalproto/eval_connect_pb2.py +37 -0
  32. evalbench/evalproto/eval_connect_pb2.pyi +15 -0
  33. evalbench/evalproto/eval_connect_pb2_grpc.py +24 -0
  34. evalbench/evalproto/eval_request_pb2.py +68 -0
  35. evalbench/evalproto/eval_request_pb2.pyi +165 -0
  36. evalbench/evalproto/eval_request_pb2_grpc.py +24 -0
  37. evalbench/evalproto/eval_response_pb2.py +37 -0
  38. evalbench/evalproto/eval_response_pb2.pyi +13 -0
  39. evalbench/evalproto/eval_response_pb2_grpc.py +24 -0
  40. evalbench/evalproto/eval_service_pb2.py +41 -0
  41. evalbench/evalproto/eval_service_pb2.pyi +8 -0
  42. evalbench/evalproto/eval_service_pb2_grpc.py +417 -0
  43. evalbench/evaluator/__init__.py +28 -0
  44. evalbench/evaluator/agentevaluator.py +263 -0
  45. evalbench/evaluator/agentorchestrator.py +55 -0
  46. evalbench/evaluator/cortadoevaluator.py +184 -0
  47. evalbench/evaluator/cortadoorchestrator.py +37 -0
  48. evalbench/evaluator/dataagentevaluator.py +205 -0
  49. evalbench/evaluator/dataagentorchestrator.py +228 -0
  50. evalbench/evaluator/dataagentvirtualuser.py +31 -0
  51. evalbench/evaluator/db_manager.py +116 -0
  52. evalbench/evaluator/evaluator.py +292 -0
  53. evalbench/evaluator/interactevaluator.py +190 -0
  54. evalbench/evaluator/interactorchestrator.py +229 -0
  55. evalbench/evaluator/oneshotorchestrator.py +286 -0
  56. evalbench/evaluator/orchestrator.py +71 -0
  57. evalbench/evaluator/progress_reporter.py +343 -0
  58. evalbench/evaluator/simulateduser.py +55 -0
  59. evalbench/evaluator/streamingorchestrator.py +205 -0
  60. evalbench/evaluator/virtualuser.py +31 -0
  61. evalbench/generators/models/__init__.py +49 -0
  62. evalbench/generators/models/alloydb_ai_nl.py +29 -0
  63. evalbench/generators/models/claude.py +43 -0
  64. evalbench/generators/models/claude_code.py +801 -0
  65. evalbench/generators/models/codex_cli.py +813 -0
  66. evalbench/generators/models/gemini.py +67 -0
  67. evalbench/generators/models/gemini_cli.py +1054 -0
  68. evalbench/generators/models/generator.py +33 -0
  69. evalbench/generators/models/grpc_proxy.py +123 -0
  70. evalbench/generators/models/passthrough.py +11 -0
  71. evalbench/generators/models/query_data_api.py +84 -0
  72. evalbench/generators/models/querydata.py +162 -0
  73. evalbench/generators/prompts/__init__.py +25 -0
  74. evalbench/generators/prompts/dataagentinteractuser.py +188 -0
  75. evalbench/generators/prompts/generator.py +17 -0
  76. evalbench/generators/prompts/interactsystem.py +90 -0
  77. evalbench/generators/prompts/interactuser.py +186 -0
  78. evalbench/generators/prompts/passthrough.py +13 -0
  79. evalbench/generators/prompts/simulateduser.py +46 -0
  80. evalbench/generators/prompts/sqlgenbase.py +276 -0
  81. evalbench/mp/__init__.py +0 -0
  82. evalbench/mp/mprunner.py +48 -0
  83. evalbench/reporting/__init__.py +21 -0
  84. evalbench/reporting/analyzer.py +210 -0
  85. evalbench/reporting/bqstore.py +200 -0
  86. evalbench/reporting/csv.py +41 -0
  87. evalbench/reporting/gcs_artifact.py +168 -0
  88. evalbench/reporting/report.py +48 -0
  89. evalbench/repository/__init__.py +5 -0
  90. evalbench/repository/base.py +10 -0
  91. evalbench/repository/nldRepo.py +39 -0
  92. evalbench/scorers/__init__.py +0 -0
  93. evalbench/scorers/behavioralmetrics.py +85 -0
  94. evalbench/scorers/binaryrubricscorer.py +87 -0
  95. evalbench/scorers/comparator.py +137 -0
  96. evalbench/scorers/dataformscorer.py +147 -0
  97. evalbench/scorers/dbtscorer.py +146 -0
  98. evalbench/scorers/endtoendlatency.py +89 -0
  99. evalbench/scorers/exact_match_consistency_comparator.py +55 -0
  100. evalbench/scorers/exactmatcher.py +50 -0
  101. evalbench/scorers/examples/sample_python_validator.py +50 -0
  102. evalbench/scorers/executablesql.py +50 -0
  103. evalbench/scorers/generatedqueryregexpmatcher.py +113 -0
  104. evalbench/scorers/goalcompletionrate.py +67 -0
  105. evalbench/scorers/llm_consistency_comparator.py +235 -0
  106. evalbench/scorers/llmrater.py +327 -0
  107. evalbench/scorers/multi_trial_comparator.py +50 -0
  108. evalbench/scorers/multi_trial_score.py +149 -0
  109. evalbench/scorers/parameteranalysis.py +67 -0
  110. evalbench/scorers/prompt/behavioralmetrics.py +23 -0
  111. evalbench/scorers/prompt/binaryrubricscorer.py +13 -0
  112. evalbench/scorers/prompt/goalcompletion.py +16 -0
  113. evalbench/scorers/prompt/parameteranalysis.py +34 -0
  114. evalbench/scorers/prompt/skillsbestpractices.py +64 -0
  115. evalbench/scorers/pythonscorer.py +93 -0
  116. evalbench/scorers/recallmatcher.py +123 -0
  117. evalbench/scorers/returnedsql.py +58 -0
  118. evalbench/scorers/score.py +203 -0
  119. evalbench/scorers/setmatcher.py +79 -0
  120. evalbench/scorers/skillsbestpractices.py +204 -0
  121. evalbench/scorers/skillstrajectorymatcher.py +108 -0
  122. evalbench/scorers/tokenconsumption.py +82 -0
  123. evalbench/scorers/toolcalllatency.py +80 -0
  124. evalbench/scorers/trajectorymatcher.py +138 -0
  125. evalbench/scorers/turncount.py +67 -0
  126. evalbench/scorers/util.py +63 -0
  127. evalbench/test/__init__.py +0 -0
  128. evalbench/test/alloydb_test.py +29 -0
  129. evalbench/test/bigtable_test.py +79 -0
  130. evalbench/test/binaryrubricscorer_test.py +103 -0
  131. evalbench/test/dbtscorer_test.py +172 -0
  132. evalbench/test/evalbench_test.py +84 -0
  133. evalbench/test/evaluator_test.py +211 -0
  134. evalbench/test/exact_match_consistency_comparator_test.py +75 -0
  135. evalbench/test/gcs_artifact_test.py +116 -0
  136. evalbench/test/llm_consistency_comparator_test.py +196 -0
  137. evalbench/test/llmrater_test.py +122 -0
  138. evalbench/test/mongodb_test.py +689 -0
  139. evalbench/test/multi_trial_score_test.py +228 -0
  140. evalbench/test/multi_trial_scorework_test.py +55 -0
  141. evalbench/test/oneshotorchestrator_test.py +157 -0
  142. evalbench/test/pythonscorer_test.py +92 -0
  143. evalbench/test/query_data_api_test.py +114 -0
  144. evalbench/test/robustness_test.py +197 -0
  145. evalbench/test/sessionmgr_test.py +52 -0
  146. evalbench/test/set_matcher_test.py +70 -0
  147. evalbench/test/spanner_test.py +49 -0
  148. evalbench/test/sqlgenwork_test.py +76 -0
  149. evalbench/test/test_db_cleanups.py +41 -0
  150. evalbench/test/test_spanner_cleanup.py +68 -0
  151. evalbench/test/trajectory_matcher_test.py +85 -0
  152. evalbench/util/__init__.py +9 -0
  153. evalbench/util/auth.py +112 -0
  154. evalbench/util/config.py +237 -0
  155. evalbench/util/context.py +3 -0
  156. evalbench/util/fake_mcp_server.py +81 -0
  157. evalbench/util/flags.py +7 -0
  158. evalbench/util/gcp.py +24 -0
  159. evalbench/util/interactutil.py +145 -0
  160. evalbench/util/loghandler.py +25 -0
  161. evalbench/util/progress.py +31 -0
  162. evalbench/util/rate_limit.py +41 -0
  163. evalbench/util/sanitizer.py +18 -0
  164. evalbench/util/scriptrunner.py +81 -0
  165. evalbench/util/service.py +17 -0
  166. evalbench/util/session.py +37 -0
  167. evalbench/util/sessionmgr.py +148 -0
  168. evalbench/util/setup_databases.py +83 -0
  169. evalbench/util/test_setup_databases.py +39 -0
  170. evalbench/work/__init__.py +5 -0
  171. evalbench/work/agentgenwork.py +67 -0
  172. evalbench/work/agentscorework.py +67 -0
  173. evalbench/work/dataagentvuserwork.py +32 -0
  174. evalbench/work/interactsqlexecwork.py +138 -0
  175. evalbench/work/multi_trial_scorework.py +52 -0
  176. evalbench/work/promptgenwork.py +31 -0
  177. evalbench/work/scorework.py +38 -0
  178. evalbench/work/sqlexecwork.py +193 -0
  179. evalbench/work/sqlgeninteractwork.py +35 -0
  180. evalbench/work/sqlgenquerydatawork.py +38 -0
  181. evalbench/work/sqlgenwork.py +61 -0
  182. evalbench/work/vuserwork.py +32 -0
  183. evalbench/work/work.py +21 -0
  184. google_evalbench-1.7.1.dist-info/METADATA +358 -0
  185. google_evalbench-1.7.1.dist-info/RECORD +189 -0
  186. google_evalbench-1.7.1.dist-info/WHEEL +5 -0
  187. google_evalbench-1.7.1.dist-info/entry_points.txt +2 -0
  188. google_evalbench-1.7.1.dist-info/licenses/LICENSE +202 -0
  189. google_evalbench-1.7.1.dist-info/top_level.txt +1 -0
evalbench/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ import os
2
+ import sys
3
+
4
+ # Append package root to sys.path for legacy absolute imports.
5
+ # Using append (rather than insert) prevents namespace collisions in spawned child processes.
6
+ sys.path.append(os.path.dirname(__file__))
7
+
8
+
9
+ from . import reporting
10
+ from . import util
11
+ from . import dataset
12
+ from . import evaluator
@@ -0,0 +1,149 @@
1
+ import asyncio
2
+ import os
3
+ from aiologger import Logger
4
+ import grpc
5
+ from evalproto import eval_request_pb2, eval_connect_pb2, eval_config_pb2
6
+ from evalproto import eval_service_pb2_grpc
7
+ import random
8
+ import argparse
9
+ import google.oauth2.id_token
10
+ import google.auth.transport.requests
11
+
12
+
13
+ def get_id_token(audience):
14
+ """
15
+ Fetches an ID token for the specified audience.
16
+ """
17
+ # The request object is used to make the HTTP call to fetch the token
18
+ request = google.auth.transport.requests.Request()
19
+
20
+ # This will search for credentials in:
21
+ # 1. Environment variables (GOOGLE_APPLICATION_CREDENTIALS)
22
+ # 2. Metadata server (if running on GCP)
23
+ token = google.oauth2.id_token.fetch_id_token(request, audience)
24
+
25
+ return token
26
+
27
+
28
+ class EvalbenchClient:
29
+ def __init__(self, endpoint: str):
30
+ self.endpoint = endpoint
31
+ if self.endpoint == "local":
32
+ host = os.getenv("EVALBENCH_HOST", "localhost")
33
+ port = os.getenv("PORT", "50051")
34
+ address = f"{host}:{port}"
35
+ if os.getenv("EVALBENCH_INSECURE", "").lower() == "true":
36
+ self.channel = grpc.aio.insecure_channel(address)
37
+ else:
38
+ channel_creds = grpc.alts_channel_credentials()
39
+ self.channel = grpc.aio.secure_channel(address, channel_creds)
40
+ else:
41
+ address = f"{self.endpoint}:443"
42
+ id_token = get_id_token(f"https://{self.endpoint}")
43
+ # 2. Create Call Credentialss
44
+ # This puts the token in the 'authorization: Bearer <token>' header
45
+ call_creds = grpc.access_token_call_credentials(id_token)
46
+
47
+ # 3. Create SSL Channel Credentials
48
+ # ID tokens require a secure (TLS) channel
49
+ channel_creds = grpc.ssl_channel_credentials()
50
+
51
+ # 4. Composite Credentials
52
+ # Combine the SSL channel with the token-based call credentials
53
+ composite_creds = grpc.composite_channel_credentials(
54
+ channel_creds, call_creds)
55
+ self.channel = grpc.aio.secure_channel(address, composite_creds)
56
+
57
+ self.stub = eval_service_pb2_grpc.EvalServiceStub(self.channel)
58
+ rpc_id = "{:032x}".format(random.getrandbits(128))
59
+ self.metadata = grpc.aio.Metadata(
60
+ ("client-rpc-id", rpc_id),
61
+ )
62
+
63
+ async def ping(self):
64
+ request = eval_request_pb2.PingRequest()
65
+ response = await self.stub.Ping(request, metadata=self.metadata)
66
+ return response
67
+
68
+ async def connect(self):
69
+ request = eval_connect_pb2.EvalConnectRequest()
70
+ request.client_id = "me"
71
+ response = await self.stub.Connect(request, metadata=self.metadata)
72
+ return response
73
+
74
+ async def set_evalconfig(self, experiment: str):
75
+ data = None
76
+ with open(experiment, "rb") as f:
77
+ data = f.read()
78
+ request = eval_config_pb2.EvalConfigRequest()
79
+ request.yaml_config = data
80
+ response = await self.stub.EvalConfig(request, metadata=self.metadata)
81
+ return response
82
+
83
+ async def get_evalinputs(self):
84
+ request = eval_request_pb2.EvalInputRequest()
85
+ get_evalinputs_stream = self.stub.ListEvalInputs(
86
+ request, metadata=self.metadata
87
+ )
88
+ while True:
89
+ response = await get_evalinputs_stream.read()
90
+ if response == grpc.aio.EOF:
91
+ break
92
+ yield response
93
+
94
+ async def eval(self, evalinputs):
95
+ eval_call = self.stub.Eval(metadata=self.metadata)
96
+ for eval_input in evalinputs:
97
+ await eval_call.write(eval_input)
98
+ await eval_call.done_writing()
99
+ response = await eval_call
100
+ return response
101
+
102
+
103
+ async def run(experiment: str, endpoint: str) -> None:
104
+ logger = Logger.with_default_handlers(name="evalbench-logger")
105
+ evalbenchclient = EvalbenchClient(endpoint)
106
+ response = await evalbenchclient.ping()
107
+ logger.info(f"ping Returned: {response.response}")
108
+
109
+ response = await evalbenchclient.connect()
110
+ logger.info(f"connect Returned: {response.response}")
111
+
112
+ response = await evalbenchclient.ping()
113
+ logger.info(f"ping Returned: {response.response}")
114
+
115
+ response = await evalbenchclient.set_evalconfig(experiment)
116
+ logger.info(f"get_evalinput Returned: {response.response}")
117
+
118
+ evalInputs = []
119
+ async for response in evalbenchclient.get_evalinputs():
120
+ evalInputs.append(response)
121
+ logger.info(f"evalInputs: {len(evalInputs)}")
122
+ response = await evalbenchclient.eval(evalInputs)
123
+ logger.info(f"eval Returned: {response.response}")
124
+
125
+
126
+ async def main():
127
+ logger = Logger.with_default_handlers(name="evalbench-logger")
128
+ parser = argparse.ArgumentParser()
129
+ parser.add_argument("--experiment", dest="experiment")
130
+ parser.add_argument("--endpoint", dest="endpoint")
131
+ known_args, _ = parser.parse_known_args()
132
+
133
+ await run(known_args.experiment, known_args.endpoint)
134
+ # get a set of all running tasks
135
+ all_tasks = asyncio.all_tasks()
136
+ # get the current tasks
137
+ current_task = asyncio.current_task()
138
+ # remove the current task from the list of all tasks
139
+ all_tasks.remove(current_task)
140
+ # report a message
141
+ print(f"Main waiting for {len(all_tasks)} tasks...")
142
+ # suspend until all tasks are completed
143
+ if len(all_tasks) > 0:
144
+ await asyncio.wait(all_tasks)
145
+ await logger.shutdown()
146
+
147
+
148
+ if __name__ == "__main__":
149
+ asyncio.run(main())
@@ -0,0 +1,42 @@
1
+ from .postgres import PGDB
2
+ from .mysql import MySQLDB
3
+ from .sqlserver import SQLServerDB
4
+ from .sqlite import SQLiteDB
5
+ from .db import DB
6
+ from .bigquery import BQDB
7
+ from .bigtable import BigtableDB
8
+ from .alloydb import AlloyDB
9
+ from .alloydb_omni import AlloyDBOmni
10
+ from .spanner import SpannerDB
11
+ from .mongodb import MongoDB
12
+
13
+
14
+ def get_database(db_config, db_name) -> DB:
15
+ # if db_name is provided:
16
+ # - It will override the provided default database_name
17
+ # - This is useful as the default db may be "postgres" or a default only used for setup
18
+ if db_name:
19
+ suffix = db_config.get("db_name_suffix", "")
20
+ db_config["database_name"] = f"{db_name}{suffix}"
21
+
22
+ if db_config["db_type"] == "postgres":
23
+ return PGDB(db_config)
24
+ if db_config["db_type"] == "spanner":
25
+ return SpannerDB(db_config)
26
+ if db_config["db_type"] == "mysql":
27
+ return MySQLDB(db_config)
28
+ if db_config["db_type"] == "sqlserver":
29
+ return SQLServerDB(db_config)
30
+ if db_config["db_type"] == "sqlite":
31
+ return SQLiteDB(db_config)
32
+ if db_config["db_type"] == "bigquery":
33
+ return BQDB(db_config)
34
+ if db_config["db_type"] == "alloydb":
35
+ return AlloyDB(db_config)
36
+ if db_config["db_type"] == "alloydb_omni":
37
+ return AlloyDBOmni(db_config)
38
+ if db_config["db_type"] == "bigtable":
39
+ return BigtableDB(db_config)
40
+ if db_config["db_type"] == "mongodb":
41
+ return MongoDB(db_config)
42
+ raise ValueError("DB Type not Supported")
@@ -0,0 +1,49 @@
1
+
2
+ from .db import DB
3
+ from .postgres import PGDB
4
+ import sqlalchemy
5
+ from sqlalchemy.pool import NullPool
6
+ from google.cloud.alloydb.connector import Connector as AlloyDBConnector
7
+ from google.cloud.alloydb.connector import IPTypes as AlloyDBIPTypes
8
+
9
+ CONNECTOR = AlloyDBConnector()
10
+
11
+
12
+ class AlloyDB(PGDB):
13
+ def __init__(self, db_config):
14
+ """
15
+ Initializes the AlloyDB connection, overriding the PGDB's
16
+ default Google Cloud SQL connection mechanism.
17
+ """
18
+ super().__init__(db_config)
19
+ self.nl_config = db_config['nl_config']
20
+
21
+ if 'api_endpoint' in db_config:
22
+ CONNECTOR._alloydb_api_endpoint = db_config['api_endpoint']
23
+
24
+ def get_conn_alloydb():
25
+ return CONNECTOR.connect(
26
+ self.db_path,
27
+ "pg8000",
28
+ user=self.username,
29
+ password=self.password,
30
+ db=self.db_name,
31
+ enable_iam_auth=self.use_adc, # handled in PGDB
32
+ ip_type=AlloyDBIPTypes.PUBLIC,
33
+ )
34
+
35
+ def get_engine_args_alloydb():
36
+ common_args = {
37
+ "creator": get_conn_alloydb,
38
+ "connect_args": {"command_timeout": 60},
39
+ }
40
+ if "is_tmp_db" in db_config:
41
+ common_args["poolclass"] = NullPool
42
+ else:
43
+ common_args["pool_size"] = 50
44
+ common_args["pool_recycle"] = 300
45
+ return common_args
46
+
47
+ self.engine = sqlalchemy.create_engine(
48
+ "postgresql+pg8000://", **get_engine_args_alloydb()
49
+ )
@@ -0,0 +1,43 @@
1
+
2
+ from .db import DB
3
+ from .postgres import PGDB
4
+ import sqlalchemy
5
+ import pg8000
6
+ from sqlalchemy.pool import NullPool
7
+
8
+
9
+ class AlloyDBOmni(PGDB):
10
+ def __init__(self, db_config):
11
+ """
12
+ Initializes the AlloyDB connection, overriding the PGDB's
13
+ default Google Cloud SQL connection mechanism.
14
+ """
15
+ super().__init__(db_config)
16
+ self.nl_config = db_config['nl_config']
17
+ self.host = db_config.get("host", "localhost")
18
+ self.port = db_config.get("port", 5432)
19
+
20
+ def get_conn_alloydb():
21
+ return pg8000.connect(
22
+ user=self.username,
23
+ password=self.password,
24
+ host=self.host,
25
+ port=self.port,
26
+ database=self.db_name
27
+ )
28
+
29
+ def get_engine_args_alloydb():
30
+ common_args = {
31
+ "creator": get_conn_alloydb,
32
+ "connect_args": {"command_timeout": 60},
33
+ }
34
+ if "is_tmp_db" in db_config:
35
+ common_args["poolclass"] = NullPool
36
+ else:
37
+ common_args["pool_size"] = 50
38
+ common_args["pool_recycle"] = 300
39
+ return common_args
40
+
41
+ self.engine = sqlalchemy.create_engine(
42
+ "postgresql+pg8000://", **get_engine_args_alloydb()
43
+ )
@@ -0,0 +1,297 @@
1
+ from google.cloud import bigquery
2
+ import logging
3
+ import re
4
+ from .db import DB
5
+ from .util import with_cache_execute, DatabaseSchema
6
+ from util.rate_limit import rate_limit, ResourceExhaustedError
7
+ from typing import List, Optional, Tuple, Any, Dict
8
+ import json
9
+ import sqlparse
10
+ from google.cloud.bigquery import QueryJobConfig, ConnectionProperty
11
+ from util.gcp import get_gcp_project
12
+ from google.api_core.exceptions import GoogleAPICallError
13
+
14
+
15
+ class BQDB(DB):
16
+
17
+ #####################################################
18
+ #####################################################
19
+ # Database Connection Setup Logic
20
+ #####################################################
21
+ #####################################################
22
+
23
+ def __init__(self, db_config):
24
+ super().__init__(db_config)
25
+ self.project_id = get_gcp_project("")
26
+ self.location = db_config.get("location", "US")
27
+ self.client = bigquery.Client(project=self.project_id)
28
+ self.tmp_users = []
29
+
30
+ #####################################################
31
+ #####################################################
32
+ # Database Specific Execution Logic and Handling
33
+ #####################################################
34
+ #####################################################
35
+
36
+ def _execute_queries(self, query: str, job_config: Optional[bigquery.QueryJobConfig] = None) -> List:
37
+ result: List = []
38
+ for sub_query in sqlparse.split(query):
39
+ if sub_query:
40
+ resultset = self.client.query(sub_query, job_config)
41
+ rows = resultset.result()
42
+ if rows:
43
+ for row in rows:
44
+ result.append(dict(row))
45
+ return result
46
+
47
+ def batch_execute(self, commands: list[str]):
48
+ for command in commands:
49
+ self.execute(command)
50
+
51
+ def execute(
52
+ self, query: str, eval_query: Optional[str] = None, use_cache=False, rollback=False
53
+ ) -> Tuple[Any, Any, Any]:
54
+ if query.strip() == "":
55
+ return None, None, None
56
+ if not use_cache or not self.cache_client or eval_query:
57
+ return self._execute(query, eval_query, rollback)
58
+ return with_cache_execute(
59
+ query, f"{self.project_id}.{self.db_name}", self._execute, self.cache_client
60
+ )
61
+
62
+ def _execute(
63
+ self, query: str, eval_query: Optional[str] = None, rollback=False
64
+ ) -> Tuple[Any, Any, Any]:
65
+ def _run_execute(query: str, eval_query: Optional[str] = None, rollback=False):
66
+ result: List = []
67
+ eval_result: List = []
68
+ error = None
69
+ query_replaced = query.replace("{{dataset}}", self.db_name)
70
+ if eval_query is not None:
71
+ eval_query_replaced = eval_query.replace(
72
+ "{{dataset}}", self.db_name)
73
+ try:
74
+ if rollback:
75
+ try:
76
+ initial_query = "SELECT 1;"
77
+ job_config = QueryJobConfig(create_session=True)
78
+ init_job = self.client.query(
79
+ initial_query, job_config=job_config)
80
+ init_job.result()
81
+ session_id = init_job.session_info.session_id
82
+ conn_props = [ConnectionProperty(
83
+ key="session_id", value=session_id)]
84
+
85
+ self.client.query(
86
+ "BEGIN TRANSACTION;",
87
+ job_config=QueryJobConfig(
88
+ connection_properties=conn_props)
89
+ ).result()
90
+
91
+ result = self._execute_queries(
92
+ query_replaced, job_config=QueryJobConfig(connection_properties=conn_props))
93
+
94
+ if eval_query:
95
+ eval_result = self._execute_queries(
96
+ eval_query_replaced, job_config=QueryJobConfig(connection_properties=conn_props))
97
+
98
+ self.client.query(
99
+ "ROLLBACK TRANSACTION;",
100
+ job_config=QueryJobConfig(
101
+ connection_properties=conn_props)
102
+ ).result()
103
+
104
+ except Exception as e:
105
+ error = str(e)
106
+ print(f"Error: {error}")
107
+
108
+ finally:
109
+ if 'session_id' in locals():
110
+ self.client.query(
111
+ "CALL BQ.ABORT_SESSION();",
112
+ job_config=QueryJobConfig(
113
+ connection_properties=conn_props)
114
+ ).result()
115
+ if not rollback:
116
+ result = self._execute_queries(query_replaced)
117
+
118
+ if eval_query and not rollback:
119
+ eval_result = self._execute_queries(eval_query_replaced)
120
+
121
+ except (GoogleAPICallError, Exception) as e:
122
+ error = str(e)
123
+ if "resources exceeded" in error:
124
+ raise ResourceExhaustedError(
125
+ f"BigQuery resources exhausted: {e}") from e
126
+ elif "quota exceeded" in error:
127
+ raise ResourceExhaustedError(
128
+ f"BigQuery quota exceeded: {e}") from e
129
+ else:
130
+ print(error)
131
+
132
+ return result, eval_result, error
133
+
134
+ try:
135
+ return rate_limit(
136
+ (query, eval_query, rollback),
137
+ _run_execute,
138
+ self.execs_per_minute,
139
+ self.semaphore,
140
+ self.max_attempts,
141
+ )
142
+ except ResourceExhaustedError as e:
143
+ logging.info(
144
+ "Resource Exhausted on Postgres DB. Giving up execution. Try reducing execs_per_minute."
145
+ )
146
+ return None, None, None
147
+
148
+ def get_metadata(self) -> dict:
149
+ metadata = {}
150
+ try:
151
+ for table in self.client.list_tables(self.db_name):
152
+ schema = self.client.get_table(table.reference).schema
153
+ metadata[table.table_id] = [
154
+ {"name": f.name, "type": f.field_type} for f in schema]
155
+ except Exception as e:
156
+ print(
157
+ f"Error while fetching metadata for dataset '{self.db_name}': {e}")
158
+ return metadata
159
+
160
+ #####################################################
161
+ #####################################################
162
+ # Setup / Teardown of temporary databases
163
+ #####################################################
164
+ #####################################################
165
+
166
+ def generate_ddl(self, schema: DatabaseSchema) -> List[str]:
167
+ ddl_statements = []
168
+ try:
169
+ for table in schema.tables:
170
+ columns = ", ".join(
171
+ [f"{col.name} {col.type}" for col in table.columns])
172
+ ddl_statements.append(
173
+ f"CREATE TABLE `{self.project_id}.{self.db_name}.{table.name}` ({columns})"
174
+ )
175
+ except Exception as e:
176
+ print(f"Error generating DDL statements: {e}")
177
+ return ddl_statements
178
+
179
+ def create_tmp_database(self, database_name: str):
180
+ dataset_ref = bigquery.Dataset(f"{self.project_id}.{database_name}")
181
+ dataset_ref.location = self.location
182
+ self.client.create_dataset(dataset_ref, exists_ok=True)
183
+ self.tmp_dbs.append(database_name)
184
+
185
+ def drop_tmp_database(self, database_name: str):
186
+ try:
187
+ self.client.delete_dataset(
188
+ dataset=database_name,
189
+ delete_contents=True,
190
+ not_found_ok=True,
191
+ )
192
+ if database_name in self.tmp_dbs:
193
+ self.tmp_dbs.remove(database_name)
194
+ except Exception as e:
195
+ logging.warning(f"Failed to drop dataset {database_name}: {e}")
196
+
197
+ def drop_all_tables(self):
198
+ try:
199
+ tables = list(self.client.list_tables(self.db_name))
200
+
201
+ if tables:
202
+ for table in tables:
203
+ full_table_id = f"{self.project_id}.{self.db_name}.{table.table_id}"
204
+ self.client.delete_table(full_table_id)
205
+
206
+ except Exception as e:
207
+ raise RuntimeError(
208
+ f"Failed to drop tables in dataset {self.db_name}: {e}")
209
+
210
+ def _is_float(self, value) -> bool:
211
+ try:
212
+ float(value)
213
+ except ValueError:
214
+ return False
215
+ return True
216
+
217
+ def _get_column_name_to_type_mapping(self, sql_statements: List[str]) -> Dict[str, Dict[str, str]]:
218
+ schema_mapping = {}
219
+
220
+ for statement in sql_statements:
221
+ table_match = re.search(
222
+ r'CREATE TABLE\s+`{{dataset}}\.(\w+)`', statement)
223
+ if not table_match:
224
+ continue
225
+ table_name = table_match.group(1)
226
+ column_section_match = re.search(
227
+ r'\(\n(.*?)\n\)', statement, re.DOTALL)
228
+ if not column_section_match:
229
+ continue
230
+ columns_raw = column_section_match.group(1).split(",\n")
231
+ column_type_mapping = {}
232
+ for col in columns_raw:
233
+ if col.strip().startswith("PRIMARY KEY"):
234
+ continue
235
+ col_parts = col.strip().split()
236
+ if len(col_parts) >= 2:
237
+ column_name = col_parts[0]
238
+ column_type = col_parts[1]
239
+ column_type_mapping[column_name] = column_type
240
+
241
+ schema_mapping[table_name] = column_type_mapping
242
+
243
+ return schema_mapping
244
+
245
+ def insert_data(self, data: dict[str, List[str]], setup: Optional[List[str]] = None):
246
+ if not data:
247
+ return
248
+ schema_mapping = self._get_column_name_to_type_mapping(setup)
249
+ insertion_statements = []
250
+
251
+ for table_name in data:
252
+ column_names = list(schema_mapping[table_name].keys())
253
+ for row in data[table_name]:
254
+ formatted_values = []
255
+
256
+ for index, value in enumerate(row):
257
+ col_name = column_names[index]
258
+ col_type = schema_mapping[table_name][col_name].upper()
259
+
260
+ if col_type == 'BOOL':
261
+ if value == "'1'":
262
+ formatted_values.append("TRUE")
263
+ elif value == "'0'":
264
+ formatted_values.append("FALSE")
265
+ else:
266
+ formatted_values.append(f"{value}")
267
+ elif self._is_float(value):
268
+ formatted_values.append(f"{value}")
269
+ elif col_type == 'JSON':
270
+ formatted_values.append(f"PARSE_JSON({value})")
271
+ else:
272
+ escaped_value = value.replace("''", "\\'")
273
+ formatted_values.append(f"{escaped_value}")
274
+
275
+ inline_columns = ", ".join(formatted_values)
276
+ insertion_statements.append(
277
+ f"INSERT INTO `{self.project_id}.{self.db_name}.{table_name}` VALUES ({inline_columns});"
278
+ )
279
+ try:
280
+ self.batch_execute(insertion_statements)
281
+ except RuntimeError as error:
282
+ raise RuntimeError(f"Could not insert data into database: {error}")
283
+
284
+ #####################################################
285
+ #####################################################
286
+ # Database User Management
287
+ #####################################################
288
+ #####################################################
289
+
290
+ def close_connections(self):
291
+ pass
292
+
293
+ def create_tmp_users(self, dql_user: str, dml_user: str, tmp_password: str):
294
+ pass
295
+
296
+ def delete_tmp_user(self, username: str):
297
+ pass