Warning, file /panda-server/pandaserver/asyncprocess/tests/db_read_tests.py was not indexed
or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).
0001
0002
0003 import datetime
0004 import unittest
0005 from typing import Any
0006 from unittest import mock
0007
0008 from pandaserver.asyncprocess.db_read import schema_info
0009 from pandaserver.asyncprocess.db_read.compose import compose_statement, escape_prefix
0010 from pandaserver.asyncprocess.db_read.request_shape import (
0011 MAX_KEYS,
0012 FromQuery,
0013 Query,
0014 QueryRejected,
0015 TablePlan,
0016 check_query,
0017 count_touched_partitions,
0018 parse_query,
0019 )
0020 from pandaserver.asyncprocess.db_read.schema_info import (
0021 KIND_LOB,
0022 KIND_NUMBER,
0023 KIND_OTHER,
0024 KIND_TEXT,
0025 KIND_TIME,
0026 STATEMENT_COMMENT,
0027 IndexInfo,
0028 PartitionInfo,
0029 TableInfo,
0030 TableNotVisible,
0031 get_table_info,
0032 read_table_info,
0033 )
0034 from pandaserver.taskbuffer.WrappedCursor import convert_query_in_printf_format
0035
0036 DAY = datetime.timedelta(days=1)
0037 D0 = datetime.datetime(2026, 10, 1)
0038
0039 JOB_COLUMNS = {
0040 "pandaid": KIND_NUMBER,
0041 "jeditaskid": KIND_NUMBER,
0042 "jobstatus": KIND_TEXT,
0043 "computingsite": KIND_TEXT,
0044 "modificationtime": KIND_TIME,
0045 }
0046
0047
0048 JOBS_ACTIVE4 = TableInfo(
0049 "ATLAS_PANDA",
0050 "jobsActive4",
0051 dict(JOB_COLUMNS),
0052 (IndexInfo("pandaid", False), IndexInfo("jeditaskid", False), IndexInfo("jobstatus", False), IndexInfo("computingsite", False)),
0053 None,
0054 )
0055 JOBS_DEFINED4 = TableInfo("ATLAS_PANDA", "jobsDefined4", dict(JOB_COLUMNS), (IndexInfo("pandaid", False),), None)
0056
0057
0058 JOBS_ARCHIVED4 = TableInfo(
0059 "ATLAS_PANDA",
0060 "jobsArchived4",
0061 dict(JOB_COLUMNS),
0062 (IndexInfo("pandaid", True), IndexInfo("jeditaskid", True)),
0063 PartitionInfo("modificationtime", ((None, D0), (D0, D0 + DAY), (D0 + DAY, D0 + 2 * DAY), (D0 + 2 * DAY, D0 + 3 * DAY), (D0 + 3 * DAY, D0 + 4 * DAY))),
0064 )
0065
0066 JEDI_TASKS = TableInfo(
0067 "ATLAS_PANDA",
0068 "JEDI_Tasks",
0069 {"jeditaskid": KIND_NUMBER, "status": KIND_TEXT, "modificationtime": KIND_TIME, "splitrule": KIND_TEXT},
0070 (IndexInfo("jeditaskid", True), IndexInfo("status", True)),
0071 PartitionInfo("jeditaskid", ((None, 500000), (500000, 1000000), (1000000, 1500000))),
0072 )
0073
0074 CONFIG = TableInfo(
0075 "ATLAS_PANDA",
0076 "CONFIG",
0077 {"app": KIND_TEXT, "component": KIND_TEXT, "key": KIND_TEXT, "value": KIND_TEXT, "vo": KIND_TEXT},
0078 (IndexInfo("app", False),),
0079 None,
0080 )
0081 JOB_PARAMS = TableInfo(
0082 "ATLAS_PANDA",
0083 "jobParamsTable",
0084 {"pandaid": KIND_NUMBER, "modificationtime": KIND_TIME, "jobparameters": KIND_LOB},
0085 (IndexInfo("pandaid", True),),
0086 PartitionInfo("modificationtime", ((None, D0), (D0, D0 + DAY))),
0087 )
0088
0089 INFOS = {info.table_name: info for info in (JOBS_ACTIVE4, JOBS_DEFINED4, JOBS_ARCHIVED4, JEDI_TASKS, CONFIG, JOB_PARAMS)}
0090
0091
0092 def parse(raw: dict[str, Any], position: int = 0) -> Query:
0093 return parse_query(raw, position)
0094
0095
0096 def check(raw: dict[str, Any], budget: int = 1000, from_sizes: dict[FromQuery, int] | None = None, position: int = 0) -> list[TablePlan]:
0097 return check_query(parse(raw, position), INFOS, from_sizes or {}, budget)
0098
0099
0100 class TestParseQuery(unittest.TestCase):
0101 def assertRejected(self, rule: str, raw: Any, position: int = 0) -> QueryRejected:
0102 with self.assertRaises(QueryRejected) as context:
0103 parse_query(raw, position)
0104 self.assertEqual(context.exception.rule, rule, context.exception.message)
0105 return context.exception
0106
0107 def test_full_query(self) -> None:
0108 query = parse(
0109 {
0110 "tables": ["JOBSACTIVE4", "jobsArchived4"],
0111 "where": [["PandaID", "IN", [1, 2]], ["modificationTime", ">=", {"__datetime__": "2026-10-01T00:00:00"}], ["jobStatus", "is not null"]],
0112 "group_by": ["computingSite", "day(modificationTime)"],
0113 "aggregates": [["count", "*"], ["MAX", "modificationTime"]],
0114 "order_by": [["count(*)", "desc"], "computingsite"],
0115 "limit": 10,
0116 }
0117 )
0118 self.assertEqual([table.name for table in query.tables], ["jobsActive4", "jobsArchived4"])
0119 self.assertEqual(
0120 [(c.column, c.op, c.value) for c in query.where],
0121 [("pandaid", "in", [1, 2]), ("modificationtime", ">=", "2026-10-01T00:00:00"), ("jobstatus", "is not null", None)],
0122 )
0123 self.assertEqual([key.name for key in query.group_by], ["computingsite", "day(modificationtime)"])
0124 self.assertEqual([aggregate.name for aggregate in query.aggregates], ["count(*)", "max(modificationtime)"])
0125 self.assertEqual([(key.name, key.descending) for key in query.order_by], [("count(*)", True), ("computingsite", False)])
0126 self.assertEqual(query.limit, 10)
0127
0128 def test_defaults(self) -> None:
0129 query = parse({"tables": ["CONFIG"]})
0130 self.assertEqual((query.where, query.columns, query.limit), ((), None, 1000))
0131
0132 def test_tables(self) -> None:
0133 self.assertRejected("unknown_table", {"tables": ["users"]})
0134 self.assertRejected("unknown_table", {"tables": ["jobs_StatusLog"]})
0135 self.assertRejected("bad_shape", {"tables": []})
0136 self.assertRejected("bad_shape", {"tables": ["jobsActive4", "JOBSACTIVE4"]})
0137 self.assertRejected("bad_shape", {"tables": ["jobsActive4"], "colums": ["pandaid"]})
0138
0139 def test_conditions(self) -> None:
0140 tables = ["jobsActive4"]
0141 self.assertRejected("bad_shape", {"tables": tables, "where": [["pandaid", "like", "1%"]]})
0142 self.assertRejected("bad_shape", {"tables": tables, "where": [["pandaid", "is null", 1]]})
0143 self.assertRejected("bad_shape", {"tables": tables, "where": [["pandaid", "="]]})
0144 self.assertRejected("bad_shape", {"tables": tables, "where": [["pandaid; drop", "=", 1]]})
0145 self.assertRejected("bad_value", {"tables": tables, "where": [["pandaid", "in", []]]})
0146 self.assertRejected("bad_value", {"tables": tables, "where": [["pandaid", "=", True]]})
0147 self.assertRejected("bad_value", {"tables": tables, "where": [["pandaid", "=", None]]})
0148 self.assertRejected("bad_value", {"tables": tables, "where": [["jobstatus", "prefix", ""]]})
0149 rejected = self.assertRejected("too_many_keys", {"tables": tables, "where": [["pandaid", "in", list(range(MAX_KEYS + 1))]]})
0150 self.assertEqual(rejected.detail, {"keys": MAX_KEYS + 1, "max_keys": MAX_KEYS})
0151 self.assertRejected("bad_shape", {"tables": tables, "where": [["pandaid", "=", 1]] * 21})
0152
0153 def test_from_query(self) -> None:
0154 raw = {"tables": ["jobsActive4"], "where": [["jeditaskid", "in", {"from_query": 0, "column": "jediTaskID"}]]}
0155 self.assertEqual(parse(raw, position=1).where[0].value, FromQuery(0, "jeditaskid"))
0156
0157 self.assertRejected("bad_shape", raw, position=0)
0158 self.assertRejected("bad_shape", {"tables": ["jobsActive4"], "where": [["jeditaskid", "=", {"from_query": 0, "column": "c"}]]}, position=1)
0159 self.assertRejected("bad_shape", {"tables": ["jobsActive4"], "where": [["jeditaskid", "in", {"from_query": 0}]]}, position=1)
0160
0161 def test_returned(self) -> None:
0162 tables = ["jobsActive4"]
0163 self.assertRejected("bad_shape", {"tables": tables, "columns": ["pandaid"], "group_by": ["jobstatus"]})
0164 self.assertRejected("bad_shape", {"tables": tables, "columns": []})
0165 self.assertRejected("bad_shape", {"tables": tables, "columns": ["pandaid", "PandaID"]})
0166 self.assertRejected("bad_shape", {"tables": tables, "aggregates": [["min", "*"]]})
0167 self.assertRejected("bad_shape", {"tables": tables, "aggregates": [["sum", "pandaid"]]})
0168 self.assertRejected("bad_shape", {"tables": tables, "columns": ["pandaid"], "order_by": ["jobstatus"]})
0169 self.assertRejected("bad_shape", {"tables": tables, "group_by": ["jobstatus"], "order_by": ["pandaid"]})
0170 self.assertRejected("bad_shape", {"tables": tables, "order_by": [["pandaid", "down"]]})
0171 self.assertRejected("bad_shape", {"tables": tables, "limit": 1001})
0172 self.assertRejected("bad_shape", {"tables": tables, "limit": 0})
0173 self.assertRejected("bad_shape", {"tables": tables, "limit": True})
0174
0175
0176 class TestCountTouchedPartitions(unittest.TestCase):
0177 def count(self, where: list[list[Any]], now: datetime.datetime | None = None) -> int:
0178 plans = check({"tables": ["jobsArchived4"], "where": [["pandaid", "=", 1]] + where})
0179 assert JOBS_ARCHIVED4.partitioning is not None
0180 return count_touched_partitions(JOBS_ARCHIVED4.partitioning, plans[0].where, now)
0181
0182 def test_window(self) -> None:
0183 self.assertEqual(self.count([]), 5)
0184
0185 self.assertEqual(self.count([["modificationtime", ">=", "2026-10-02T00:00:00"]]), 3)
0186 self.assertEqual(self.count([["modificationtime", ">", "2026-10-02T00:00:00"]]), 3)
0187
0188 self.assertEqual(self.count([["modificationtime", "<", "2026-10-02T00:00:00"]]), 2)
0189 self.assertEqual(self.count([["modificationtime", "<=", "2026-10-02T00:00:00"]]), 3)
0190 self.assertEqual(self.count([["modificationtime", ">=", "2026-10-02T06:00:00"], ["modificationtime", "<", "2026-10-02T18:00:00"]]), 1)
0191
0192 self.assertEqual(self.count([["modificationtime", ">=", "2026-10-01T00:00:00"], ["modificationtime", ">=", "2026-10-04T00:00:00"]]), 1)
0193
0194 self.assertEqual(self.count([["modificationtime", "<", "2026-09-01T00:00:00"]]), 1)
0195
0196 def test_points(self) -> None:
0197 self.assertEqual(self.count([["modificationtime", "=", "2026-10-02T12:00:00"]]), 1)
0198 self.assertEqual(self.count([["modificationtime", "in", ["2026-10-02T12:00:00", "2026-10-03T12:00:00", "2026-10-03T13:00:00"]]]), 2)
0199
0200 self.assertEqual(self.count([["modificationtime", "in", ["2026-10-02T00:00:00"]]]), 1)
0201
0202 self.assertEqual(self.count([["modificationtime", "in", ["2026-10-02T12:00:00"]], ["modificationtime", ">=", "2026-10-03T00:00:00"]]), 1)
0203
0204 def test_made_ahead(self) -> None:
0205
0206 now = D0 + DAY + datetime.timedelta(hours=12)
0207 self.assertEqual(self.count([], now), 3)
0208 self.assertEqual(self.count([["modificationtime", ">=", "2026-10-02T00:00:00"]], now), 1)
0209
0210 self.assertEqual(self.count([], D0 + DAY), 3)
0211
0212 self.assertEqual(self.count([["modificationtime", "=", "2026-10-04T12:00:00"]], now), 1)
0213
0214 assert JEDI_TASKS.partitioning is not None
0215 plans = check({"tables": ["JEDI_Tasks"], "where": [["jeditaskid", ">=", 600000], ["status", "=", "running"]]})
0216 self.assertEqual(count_touched_partitions(JEDI_TASKS.partitioning, plans[0].where, now), 2)
0217
0218 query = parse({"tables": ["jobsArchived4"], "where": [["pandaid", "in", [1, 2]]]}, 0)
0219 self.assertEqual(check_query(query, INFOS, {}, 1000)[0].probes, 10)
0220 self.assertEqual(check_query(query, INFOS, {}, 1000, now=now)[0].probes, 6)
0221
0222 def test_numbers(self) -> None:
0223 assert JEDI_TASKS.partitioning is not None
0224 plans = check({"tables": ["JEDI_Tasks"], "where": [["jeditaskid", "in", [10, 499999, 500000]]]})
0225 self.assertEqual(count_touched_partitions(JEDI_TASKS.partitioning, plans[0].where), 2)
0226 plans = check({"tables": ["JEDI_Tasks"], "where": [["jeditaskid", ">=", 600000], ["status", "=", "running"]]})
0227 self.assertEqual(count_touched_partitions(JEDI_TASKS.partitioning, plans[0].where), 2)
0228
0229
0230 class TestCheckQuery(unittest.TestCase):
0231 def assertRejected(self, rule: str, raw: dict[str, Any], **kwargs: Any) -> QueryRejected:
0232 with self.assertRaises(QueryRejected) as context:
0233 check(raw, **kwargs)
0234 self.assertEqual(context.exception.rule, rule, context.exception.message)
0235 return context.exception
0236
0237 def test_job_tables(self) -> None:
0238 plans = check({"tables": ["jobsDefined4", "jobsActive4", "jobsArchived4"], "where": [["pandaid", "in", [1, 2, 3]]]})
0239
0240 self.assertEqual([plan.probes for plan in plans], [3, 3, 15])
0241 plans = check({"tables": ["jobsArchived4"], "where": [["pandaid", "in", [1, 2, 3]], ["modificationtime", ">=", "2026-10-03T00:00:00"]]})
0242 self.assertEqual(plans[0].probes, 6)
0243
0244 def test_cheapest_index(self) -> None:
0245 plans = check({"tables": ["jobsActive4"], "where": [["jobstatus", "=", "running"], ["pandaid", "in", [1, 2]]]})
0246 self.assertEqual(plans[0].probes, 1)
0247
0248 def test_partition_key_index(self) -> None:
0249
0250 plans = check({"tables": ["JEDI_Tasks"], "where": [["jeditaskid", "in", [10, 600000, 1200000]]]})
0251 self.assertEqual(plans[0].probes, 3)
0252
0253 plans = check({"tables": ["JEDI_Tasks"], "where": [["status", "=", "running"]]})
0254 self.assertEqual(plans[0].probes, 3)
0255
0256 def test_budget(self) -> None:
0257 rejected = self.assertRejected("probe_budget", {"tables": ["jobsActive4", "jobsArchived4"], "where": [["pandaid", "in", [1, 2, 3]]]}, budget=17)
0258 self.assertEqual(rejected.detail, {"probes": {"jobsActive4": 3, "jobsArchived4": 15}, "budget": 17})
0259 check({"tables": ["jobsActive4", "jobsArchived4"], "where": [["pandaid", "in", [1, 2, 3]]]}, budget=18)
0260
0261 def test_no_index_key(self) -> None:
0262
0263 self.assertRejected("no_index_key", {"tables": ["jobsActive4"], "where": [["computingsite", "prefix", "CERN"]]})
0264 self.assertRejected("no_index_key", {"tables": ["jobsActive4"], "where": [["pandaid", ">", 1]]})
0265
0266 self.assertRejected("no_index_key", {"tables": ["jobsArchived4"], "where": [["modificationtime", ">=", "2026-10-03T00:00:00"]]})
0267
0268 self.assertRejected("no_index_key", {"tables": ["CONFIG"], "where": [["component", "=", "jedi"], ["key", "=", "x"]]})
0269 self.assertRejected("no_index_key", {"tables": ["CONFIG"]})
0270 plans = check({"tables": ["CONFIG"], "where": [["app", "in", ["jedi", "pandaserver"]], ["component", "=", "jedi"], ["key", "=", "x"]]})
0271 self.assertEqual(plans[0].probes, 2)
0272
0273 def test_values(self) -> None:
0274
0275 rejected = self.assertRejected("bad_value", {"tables": ["jobsActive4"], "where": [["pandaid", "=", "1"]]})
0276 self.assertEqual(rejected.detail, {"table": "jobsActive4", "column": "pandaid"})
0277 self.assertRejected("bad_value", {"tables": ["jobsActive4"], "where": [["jobstatus", "=", 1], ["pandaid", "=", 1]]})
0278 self.assertRejected("bad_value", {"tables": ["jobsActive4"], "where": [["pandaid", "=", 1], ["modificationtime", ">", "yesterday"]]})
0279 self.assertRejected("bad_value", {"tables": ["jobsActive4"], "where": [["pandaid", "prefix", "1"]]})
0280 self.assertRejected("bad_value", {"tables": ["jobParamsTable"], "where": [["pandaid", "=", 1], ["jobparameters", "prefix", "x"]]})
0281 plans = check({"tables": ["jobsActive4"], "where": [["pandaid", "=", 1], ["modificationtime", ">", "2026-10-01T02:00:00+02:00"]]})
0282 self.assertEqual(plans[0].where[1].value, datetime.datetime(2026, 10, 1, 0, 0))
0283
0284 def test_columns(self) -> None:
0285 rejected = self.assertRejected("unknown_column", {"tables": ["jobsActive4"], "columns": ["nosuch"], "where": [["pandaid", "=", 1]]})
0286 self.assertEqual(rejected.detail, {"table": "jobsActive4", "column": "nosuch"})
0287 self.assertRejected("unknown_column", {"tables": ["jobsActive4"], "where": [["pandaid", "=", 1]], "order_by": ["nosuch"]})
0288 self.assertRejected("bad_value", {"tables": ["jobsActive4"], "where": [["pandaid", "=", 1]], "group_by": ["day(jobstatus)"]})
0289 self.assertRejected("bad_value", {"tables": ["jobParamsTable"], "where": [["pandaid", "=", 1]], "aggregates": [["max", "jobparameters"]]})
0290
0291 def test_from_query(self) -> None:
0292 reference = FromQuery(0, "pandaid")
0293 raw = {"tables": ["jobsArchived4"], "where": [["pandaid", "in", {"from_query": 0, "column": "pandaid"}]]}
0294 plans = check(raw, from_sizes={reference: 4}, position=1)
0295 self.assertEqual(plans[0].probes, 20)
0296 rejected = self.assertRejected("too_many_keys", raw, from_sizes={reference: MAX_KEYS + 1}, position=1)
0297 self.assertEqual(rejected.detail, {"keys": MAX_KEYS + 1, "max_keys": MAX_KEYS})
0298
0299
0300 class TestCompose(unittest.TestCase):
0301 def compose(self, raw: dict[str, Any], backend: str, from_values: dict[FromQuery, list[Any]] | None = None, position: int = 0) -> Any:
0302 query = parse(raw, position)
0303 from_values = from_values or {}
0304 plans = check_query(query, INFOS, {reference: len(values) for reference, values in from_values.items()}, 1000)
0305 return [compose_statement(plan, query, from_values, backend) for plan in plans]
0306
0307 def test_oracle(self) -> None:
0308 (statement,) = self.compose(
0309 {
0310 "tables": ["jobsArchived4"],
0311 "where": [
0312 ["pandaid", "in", [1, 2]],
0313 ["modificationtime", ">=", "2026-10-01T00:00:00"],
0314 ["jobstatus", "!=", "failed"],
0315 ["computingsite", "is null"],
0316 ],
0317 "columns": ["pandaid", "jobstatus"],
0318 "order_by": [["pandaid", "desc"]],
0319 "limit": 100,
0320 },
0321 "oracle",
0322 )
0323 self.assertEqual(
0324 statement.sql,
0325 "SELECT pandaid,jobstatus FROM ATLAS_PANDA.jobsArchived4 "
0326 "WHERE pandaid IN (:r0,:r1) AND modificationtime >= :r2 AND jobstatus <> :r3 AND computingsite IS NULL "
0327 "ORDER BY pandaid DESC FETCH FIRST 101 ROWS ONLY" + STATEMENT_COMMENT,
0328 )
0329 self.assertEqual(statement.binds, {":r0": 1, ":r1": 2, ":r2": D0, ":r3": "failed"})
0330 self.assertEqual((statement.names, statement.limit), (("pandaid", "jobstatus"), 100))
0331
0332 def test_grouped(self) -> None:
0333 raw = {
0334 "tables": ["jobsActive4"],
0335 "where": [["jeditaskid", "=", 7]],
0336 "group_by": ["jobstatus", "hour(modificationtime)"],
0337 "aggregates": [["count", "*"], ["min", "pandaid"]],
0338 "order_by": ["hour(modificationtime)"],
0339 }
0340 (oracle,) = self.compose(raw, "oracle")
0341 self.assertEqual(
0342 oracle.sql,
0343 "SELECT jobstatus,TRUNC(modificationtime,'HH24'),COUNT(*),MIN(pandaid) FROM ATLAS_PANDA.jobsActive4 WHERE jeditaskid = :r0 "
0344 "GROUP BY jobstatus,TRUNC(modificationtime,'HH24') ORDER BY TRUNC(modificationtime,'HH24') FETCH FIRST 1001 ROWS ONLY" + STATEMENT_COMMENT,
0345 )
0346 self.assertEqual(oracle.names, ("jobstatus", "hour(modificationtime)", "count(*)", "min(pandaid)"))
0347 (postgres,) = self.compose(raw, "postgres")
0348 self.assertIn("date_trunc('hour',modificationtime)", postgres.sql)
0349 self.assertTrue(postgres.sql.endswith(" LIMIT 1001" + STATEMENT_COMMENT))
0350
0351 def test_postgres_conversion(self) -> None:
0352
0353 (statement,) = self.compose(
0354 {
0355 "tables": ["jobsActive4"],
0356 "where": [["pandaid", "in", [1, 2]], ["computingsite", "prefix", "CERN_1%"], ["modificationtime", "<", "2026-10-01T00:00:00"]],
0357 "group_by": ["day(modificationtime)"],
0358 "aggregates": [["count", "*"]],
0359 },
0360 "postgres",
0361 )
0362 sql, params = convert_query_in_printf_format(statement.sql, [statement.binds], {})
0363
0364 self.assertEqual(convert_query_in_printf_format("SELECT 1" + STATEMENT_COMMENT, [{}], {})[0], "SELECT 1" + STATEMENT_COMMENT)
0365 self.assertEqual(
0366 sql,
0367 "SELECT date_trunc('day',modificationtime),COUNT(*) FROM ATLAS_PANDA.jobsActive4 "
0368 "WHERE pandaid IN (%s,%s) AND computingsite LIKE %s ESCAPE '!' AND modificationtime < %s "
0369 "GROUP BY date_trunc('day',modificationtime) LIMIT 1001" + STATEMENT_COMMENT,
0370 )
0371 self.assertEqual(params, [[1, 2, "CERN!_1!%%", D0]])
0372
0373 def test_default_columns_leave_out_large_objects(self) -> None:
0374 (statement,) = self.compose({"tables": ["jobParamsTable"], "where": [["pandaid", "=", 1]]}, "oracle")
0375 self.assertEqual(statement.names, ("pandaid", "modificationtime"))
0376 (statement,) = self.compose({"tables": ["jobParamsTable"], "where": [["pandaid", "=", 1]], "columns": ["jobparameters"]}, "oracle")
0377 self.assertEqual(statement.names, ("jobparameters",))
0378
0379 def test_from_query(self) -> None:
0380 raw = {"tables": ["jobsActive4"], "where": [["pandaid", "in", {"from_query": 0, "column": "pandaid"}]]}
0381 (statement,) = self.compose(raw, "oracle", from_values={FromQuery(0, "pandaid"): [5, 6]}, position=1)
0382 self.assertEqual(statement.binds, {":r0": 5, ":r1": 6})
0383
0384 self.assertEqual(self.compose(raw, "oracle", from_values={FromQuery(0, "pandaid"): []}, position=1), [None])
0385
0386 def test_escape_prefix(self) -> None:
0387 self.assertEqual(escape_prefix("a_b%c!"), "a!_b!%c!!%")
0388
0389
0390 def oracle_date(day: str) -> str:
0391 return f"TO_DATE(' {day} 00:00:00', 'SYYYY-MM-DD HH24:MI:SS', 'NLS_CALENDAR=GREGORIAN')"
0392
0393
0394 class FakeDictionary:
0395
0396 def __init__(self, answers: dict[str, list[tuple[Any, ...]]]) -> None:
0397 self.answers = answers
0398 self.statements: list[tuple[str, dict[str, Any]]] = []
0399
0400 def __call__(self, sql: str, binds: dict[str, Any]) -> list[tuple[Any, ...]]:
0401 self.statements.append((sql, binds))
0402 for view, rows in self.answers.items():
0403 if f"FROM {view} " in sql:
0404 return rows
0405 return []
0406
0407
0408 class TestSchemaInfo(unittest.TestCase):
0409 def setUp(self) -> None:
0410 schema_info._cache.clear()
0411
0412 def test_oracle_time_partitions(self) -> None:
0413 run = FakeDictionary(
0414 {
0415 "ALL_TAB_COLUMNS": [
0416 ("PANDAID", "NUMBER"),
0417 ("MODIFICATIONTIME", "DATE"),
0418 ("JOBSTATUS", "VARCHAR2"),
0419 ("STARTTIME", "TIMESTAMP(6)"),
0420 ("JOBPARAMETERS", "CLOB"),
0421 ("ROWID_X", "RAW"),
0422 ],
0423
0424 "ALL_INDEXES": [("PANDAID", "LOCAL"), ("SYS_NC00034$", "LOCAL"), ("JOBSTATUS", None)],
0425 "ALL_PART_TABLES": [("RANGE",)],
0426 "ALL_PART_KEY_COLUMNS": [("MODIFICATIONTIME",)],
0427 "ALL_TAB_PARTITIONS": [(oracle_date("2012-08-01"),), (oracle_date("2026-10-02"),), ("TIMESTAMP' 2026-10-03 00:00:00'",)],
0428 }
0429 )
0430 info = read_table_info(run, "oracle", "atlas_panda", "jobsArchived4")
0431 self.assertEqual((info.owner, info.table_name), ("ATLAS_PANDA", "JOBSARCHIVED4"))
0432 self.assertEqual(
0433 info.columns,
0434 {
0435 "pandaid": KIND_NUMBER,
0436 "modificationtime": KIND_TIME,
0437 "jobstatus": KIND_TEXT,
0438 "starttime": KIND_TIME,
0439 "jobparameters": KIND_LOB,
0440 "rowid_x": KIND_OTHER,
0441 },
0442 )
0443 self.assertEqual(info.indexes, (IndexInfo("pandaid", True), IndexInfo("jobstatus", False)))
0444 self.assertEqual(
0445 info.partitioning,
0446 PartitionInfo(
0447 "modificationtime",
0448 (
0449 (None, datetime.datetime(2012, 8, 1)),
0450 (datetime.datetime(2012, 8, 1), datetime.datetime(2026, 10, 2)),
0451 (datetime.datetime(2026, 10, 2), datetime.datetime(2026, 10, 3)),
0452 ),
0453 ),
0454 )
0455 self.assertEqual(run.statements[0][1], {":owner": "ATLAS_PANDA", ":table_name": "JOBSARCHIVED4"})
0456
0457 def test_oracle_number_partitions(self) -> None:
0458 run = FakeDictionary(
0459 {
0460 "ALL_TAB_COLUMNS": [("JEDITASKID", "NUMBER"), ("STATUS", "VARCHAR2")],
0461 "ALL_INDEXES": [("JEDITASKID", "LOCAL")],
0462 "ALL_PART_TABLES": [("RANGE",)],
0463 "ALL_PART_KEY_COLUMNS": [("JEDITASKID",)],
0464 "ALL_TAB_PARTITIONS": [("500000",), ("1000000",), ("MAXVALUE",)],
0465 }
0466 )
0467 info = read_table_info(run, "oracle", "ATLAS_PANDA", "JEDI_Tasks")
0468 assert info.partitioning is not None
0469 self.assertEqual(info.partitioning.bounds, ((None, 500000), (500000, 1000000), (1000000, None)))
0470
0471 def test_oracle_other_partitioning(self) -> None:
0472
0473 run = FakeDictionary(
0474 {
0475 "ALL_TAB_COLUMNS": [("ID", "NUMBER"), ("VO", "VARCHAR2")],
0476 "ALL_PART_TABLES": [("LIST",)],
0477 "ALL_PART_KEY_COLUMNS": [("VO",)],
0478 "ALL_TAB_PARTITIONS": [("'atlas'",), ("'other'",)],
0479 }
0480 )
0481 info = read_table_info(run, "oracle", "ATLAS_PANDA", "T")
0482 self.assertEqual(info.partitioning, PartitionInfo("vo", ((None, None), (None, None))))
0483
0484 def test_postgres(self) -> None:
0485 run = FakeDictionary(
0486 {
0487 "information_schema.columns": [
0488 ("pandaid", "bigint"),
0489 ("modificationtime", "timestamp without time zone"),
0490 ("jobstatus", "character varying"),
0491 ("metadata", "jsonb"),
0492 ],
0493 "pg_class": [("p", "RANGE (modificationtime)")],
0494 "pg_index": [("pandaid",)],
0495 "pg_inherits": [("FOR VALUES FROM ('2026-10-01 00:00:00') TO ('2026-10-02 00:00:00')",), ("DEFAULT",)],
0496 }
0497 )
0498 info = read_table_info(run, "postgres", "DOMA_PANDA", "jobsArchived4")
0499 self.assertEqual((info.owner, info.table_name), ("doma_panda", "jobsarchived4"))
0500 self.assertEqual(info.columns["metadata"], KIND_LOB)
0501 self.assertEqual(info.indexes, (IndexInfo("pandaid", True),))
0502 self.assertEqual(
0503 info.partitioning,
0504 PartitionInfo("modificationtime", ((datetime.datetime(2026, 10, 1), datetime.datetime(2026, 10, 2)), (None, None))),
0505 )
0506
0507 self.assertEqual(len(run.statements), 4)
0508 for sql, binds in run.statements:
0509 converted, params = convert_query_in_printf_format(sql, [binds], {})
0510 self.assertNotIn(":", converted)
0511 self.assertEqual(params, [["doma_panda", "jobsarchived4"]])
0512
0513 def test_postgres_not_partitioned(self) -> None:
0514 run = FakeDictionary({"information_schema.columns": [("key", "character varying")], "pg_class": [("r", None)], "pg_index": [("key",)]})
0515 info = read_table_info(run, "postgres", "doma_panda", "config")
0516 self.assertEqual((info.indexes, info.partitioning), ((IndexInfo("key", False),), None))
0517
0518 def test_not_visible(self) -> None:
0519 with self.assertRaises(TableNotVisible):
0520 read_table_info(FakeDictionary({}), "oracle", "ATLAS_PANDAARCH", "jobsArchived")
0521
0522 def test_cache(self) -> None:
0523 run = FakeDictionary({"ALL_TAB_COLUMNS": [("ID", "NUMBER")]})
0524 with mock.patch.object(schema_info.time, "monotonic", return_value=1000.0):
0525 first = get_table_info(run, "oracle", "ATLAS_PANDA", "CONFIG")
0526 self.assertIs(get_table_info(run, "oracle", "atlas_panda", "config"), first)
0527 n_statements = len(run.statements)
0528 with mock.patch.object(schema_info.time, "monotonic", return_value=1000.0 + schema_info.CACHE_SECONDS):
0529 self.assertIsNot(get_table_info(run, "oracle", "ATLAS_PANDA", "CONFIG"), first)
0530 self.assertGreater(len(run.statements), n_statements)
0531
0532
0533 if __name__ == "__main__":
0534 unittest.main()