From ca3ec729debd54529c4363dbaab351872e06f7fd Mon Sep 17 00:00:00 2001 From: Roderick Dunn Date: Sun, 12 Feb 2023 19:57:42 -0500 Subject: [PATCH 1/6] optimization hint support for oracle and mysql --- sqeleton/abcs/database_types.py | 4 ++++ sqeleton/databases/base.py | 3 +++ sqeleton/databases/bigquery.py | 3 +++ sqeleton/databases/clickhouse.py | 3 +++ sqeleton/databases/databricks.py | 3 +++ sqeleton/databases/duckdb.py | 3 +++ sqeleton/databases/postgresql.py | 3 +++ sqeleton/databases/presto.py | 3 +++ sqeleton/databases/redshift.py | 2 ++ sqeleton/databases/snowflake.py | 3 +++ sqeleton/databases/vertica.py | 3 +++ sqeleton/queries/ast_classes.py | 16 +++++++++++----- 12 files changed, 44 insertions(+), 5 deletions(-) diff --git a/sqeleton/abcs/database_types.py b/sqeleton/abcs/database_types.py index bc022b5..77c269a 100644 --- a/sqeleton/abcs/database_types.py +++ b/sqeleton/abcs/database_types.py @@ -201,6 +201,10 @@ def offset_limit(self, offset: Optional[int] = None, limit: Optional[int] = None def explain_as_text(self, query: str) -> str: "Provide SQL for explaining a query, returned as table(varchar)" + @abstractmethod + def optimizer_hints(self, hints: str) -> str: + "Provide SQL for enclosing optimizer hints" + @abstractmethod def timestamp_value(self, t: datetime) -> str: "Provide SQL for the given timestamp value" diff --git a/sqeleton/databases/base.py b/sqeleton/databases/base.py index ea705ab..f5687d7 100644 --- a/sqeleton/databases/base.py +++ b/sqeleton/databases/base.py @@ -168,6 +168,9 @@ def current_timestamp(self) -> str: def explain_as_text(self, query: str) -> str: return f"EXPLAIN {query}" + def optimizer_hints(self, s: str): + return f"/*+ {s} */ " if s else "" + def _constant_value(self, v): if v is None: return "NULL" diff --git a/sqeleton/databases/bigquery.py b/sqeleton/databases/bigquery.py index 0b4dc66..e9c2c56 100644 --- a/sqeleton/databases/bigquery.py +++ b/sqeleton/databases/bigquery.py @@ -137,6 +137,9 @@ def type_repr(self, t) -> str: def set_timezone_to_utc(self) -> str: raise NotImplementedError() + def optimizer_hints(self, hints: str) -> str: + raise NotImplementedError("Optimizer hints not yet implemented in bigquery") + class BigQuery(Database): CONNECT_URI_HELP = "bigquery:///" diff --git a/sqeleton/databases/clickhouse.py b/sqeleton/databases/clickhouse.py index 6854b07..0d40eea 100644 --- a/sqeleton/databases/clickhouse.py +++ b/sqeleton/databases/clickhouse.py @@ -162,6 +162,9 @@ def set_timezone_to_utc(self) -> str: def current_timestamp(self) -> str: return "now()" + def optimizer_hints(self, hints: str) -> str: + raise NotImplementedError("Optimizer hints not yet implemented in clickhouse") + class Clickhouse(ThreadedDatabase): dialect = Dialect() diff --git a/sqeleton/databases/databricks.py b/sqeleton/databases/databricks.py index fff3d90..f33a1fc 100644 --- a/sqeleton/databases/databricks.py +++ b/sqeleton/databases/databricks.py @@ -94,6 +94,9 @@ def _convert_db_precision_to_digits(self, p: int) -> int: def set_timezone_to_utc(self) -> str: return "SET TIME ZONE 'UTC'" + def optimizer_hints(self, hints: str) -> str: + raise NotImplementedError("Optimizer hints not yet implemented in databricks") + class Databricks(ThreadedDatabase): dialect = Dialect() diff --git a/sqeleton/databases/duckdb.py b/sqeleton/databases/duckdb.py index 07ae4f8..a4a2475 100644 --- a/sqeleton/databases/duckdb.py +++ b/sqeleton/databases/duckdb.py @@ -136,6 +136,9 @@ def set_timezone_to_utc(self) -> str: def current_timestamp(self) -> str: return "current_timestamp" + def optimizer_hints(self, hints: str) -> str: + raise NotImplementedError("Optimizer hints not yet implemented in duckdb") + class DuckDB(Database): dialect = Dialect() diff --git a/sqeleton/databases/postgresql.py b/sqeleton/databases/postgresql.py index 7c7b47d..341e8d1 100644 --- a/sqeleton/databases/postgresql.py +++ b/sqeleton/databases/postgresql.py @@ -96,6 +96,9 @@ def set_timezone_to_utc(self) -> str: def current_timestamp(self) -> str: return "current_timestamp" + def optimizer_hints(self, hints: str) -> str: + raise NotImplementedError("Optimizer hints not yet implemented in postgresql") + class PostgreSQL(ThreadedDatabase): dialect = PostgresqlDialect() diff --git a/sqeleton/databases/presto.py b/sqeleton/databases/presto.py index 3dde7db..7058798 100644 --- a/sqeleton/databases/presto.py +++ b/sqeleton/databases/presto.py @@ -141,6 +141,9 @@ def set_timezone_to_utc(self) -> str: def current_timestamp(self) -> str: return "current_timestamp" + def optimizer_hints(self, hints: str) -> str: + raise NotImplementedError("Optimizer hints not yet implemented in presto") + class Presto(Database): dialect = Dialect() diff --git a/sqeleton/databases/redshift.py b/sqeleton/databases/redshift.py index b1e6617..1e05295 100644 --- a/sqeleton/databases/redshift.py +++ b/sqeleton/databases/redshift.py @@ -57,6 +57,8 @@ def concat(self, items: List[str]) -> str: def is_distinct_from(self, a: str, b: str) -> str: return f"({a} IS NULL != {b} IS NULL) OR ({a}!={b})" + def optimizer_hints(self, hints: str) -> str: + raise NotImplementedError("Optimizer hints not yet implemented in redshift") class Redshift(PostgreSQL): dialect = Dialect() diff --git a/sqeleton/databases/snowflake.py b/sqeleton/databases/snowflake.py index ebc9bb8..5edafc0 100644 --- a/sqeleton/databases/snowflake.py +++ b/sqeleton/databases/snowflake.py @@ -136,6 +136,9 @@ def table_information(self) -> Compilable: def set_timezone_to_utc(self) -> str: return "ALTER SESSION SET TIMEZONE = 'UTC'" + def optimizer_hints(self, hints: str) -> str: + raise NotImplementedError("Optimizer hints not yet implemented in snowflake") + class Snowflake(Database): dialect = Dialect() diff --git a/sqeleton/databases/vertica.py b/sqeleton/databases/vertica.py index 3f853ea..820b170 100644 --- a/sqeleton/databases/vertica.py +++ b/sqeleton/databases/vertica.py @@ -149,6 +149,9 @@ def set_timezone_to_utc(self) -> str: def current_timestamp(self) -> str: return "current_timestamp(6)" + def optimizer_hints(self, hints: str) -> str: + raise NotImplementedError("Optimizer hints not yet implemented in vertica") + class Vertica(ThreadedDatabase): dialect = Dialect() diff --git a/sqeleton/queries/ast_classes.py b/sqeleton/queries/ast_classes.py index 5888e56..ee27a12 100644 --- a/sqeleton/queries/ast_classes.py +++ b/sqeleton/queries/ast_classes.py @@ -91,14 +91,14 @@ class ITable(AbstractTable): source_table: Any schema: Schema = None - def select(self, *exprs, distinct=SKIP, **named_exprs): + def select(self, *exprs, distinct=SKIP, optimizer_hints=SKIP, **named_exprs): """Create a new table with the specified fields""" exprs = args_as_tuple(exprs) exprs = _drop_skips(exprs) named_exprs = _drop_skips_dict(named_exprs) exprs += _named_exprs_as_aliases(named_exprs) resolve_names(self.source_table, exprs) - return Select.make(self, columns=exprs, distinct=distinct) + return Select.make(self, columns=exprs, distinct=distinct, optimizer_hints=optimizer_hints) def where(self, *exprs): exprs = args_as_tuple(exprs) @@ -682,6 +682,7 @@ class Select(ExprNode, ITable, Root): having_exprs: Sequence[Expr] = None limit_expr: int = None distinct: bool = False + optimizer_hints: Sequence[Expr] = None @property def schema(self): @@ -699,7 +700,8 @@ def compile(self, parent_c: Compiler) -> str: columns = ", ".join(map(c.compile, self.columns)) if self.columns else "*" distinct = "DISTINCT " if self.distinct else "" - select = f"SELECT {distinct}{columns}" + optimizer_hints = c.dialect.optimizer_hints(self.optimizer_hints) + select = f"SELECT {optimizer_hints}{distinct}{columns}" if self.table: select += " FROM " + c.compile(self.table) @@ -729,15 +731,19 @@ def compile(self, parent_c: Compiler) -> str: return select @classmethod - def make(cls, table: ITable, distinct: bool = SKIP, **kwargs): + def make(cls, table: ITable, distinct: bool = SKIP, optimizer_hints: str = SKIP, **kwargs): assert "table" not in kwargs if not isinstance(table, cls): # If not Select if distinct is not SKIP: kwargs["distinct"] = distinct + if optimizer_hints is not SKIP: + kwargs["optimizer_hints"] = optimizer_hints return cls(table, **kwargs) # We can safely assume isinstance(table, Select) + if optimizer_hints is not SKIP: + kwargs["optimizer_hints"] = optimizer_hints if distinct is not SKIP: if distinct == False and table.distinct: @@ -752,7 +758,7 @@ def make(cls, table: ITable, distinct: bool = SKIP, **kwargs): if getattr(table, k) is not None: if k == "where_exprs": # Additive attribute kwargs[k] = getattr(table, k) + v - elif k == "distinct": + elif k in ["distinct", "optimizer_hints"]: pass else: raise ValueError(k) From 5092994eeb0cb4522d63630ae884e0386d918271 Mon Sep 17 00:00:00 2001 From: Roderick Dunn Date: Sun, 12 Feb 2023 19:59:12 -0500 Subject: [PATCH 2/6] unit tests --- tests/test_query.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_query.py b/tests/test_query.py index 9330092..7fae5ce 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -16,6 +16,7 @@ def normalize_spaces(s: str): class MockDialect(AbstractDialect): name = "MockDialect" + PLACEHOLDER_TABLE = None ROUNDS_ON_PREC_LOSS = False def quote(self, s: str) -> str: @@ -50,6 +51,9 @@ def timestamp_value(self, t: datetime) -> str: def set_timezone_to_utc(self) -> str: return "set timezone 'UTC'" + def optimizer_hints(self, s: str): + return f"/*+ {s} */ " if s else "" + def load_mixins(self): raise NotImplementedError() @@ -189,6 +193,22 @@ def test_select_distinct(self): q = c.compile(t.select(this.b, distinct=True).select(distinct=False)) self.assertEqual(q, "SELECT * FROM (SELECT DISTINCT b FROM a) tmp2") + def test_select_with_optimizer_hints(self): + c = Compiler(MockDatabase()) + t = table("a") + + q = c.compile(t.select(this.b, optimizer_hints='PARALLEL(a 16)')) + assert q == "SELECT /*+ PARALLEL(a 16) */ b FROM a" + + q = c.compile(t.where(this.b > 10).select(this.b, optimizer_hints='PARALLEL(a 16)')) + self.assertEqual(q, "SELECT /*+ PARALLEL(a 16) */ b FROM a WHERE (b > 10)") + + q = c.compile(t.limit(10).select(this.b, optimizer_hints='PARALLEL(a 16)')) + self.assertEqual(q, "SELECT /*+ PARALLEL(a 16) */ b FROM (SELECT * FROM a LIMIT 10) tmp1") + + q = c.compile(t.select(this.a).group_by(this.b).agg(this.c).select(optimizer_hints='PARALLEL(a 16)')) + self.assertEqual(q, "SELECT /*+ PARALLEL(a 16) */ * FROM (SELECT b, c FROM (SELECT a FROM point) tmp1 GROUP BY 1) tmp2") + def test_table_ops(self): c = Compiler(MockDatabase()) a = table("a").select(this.x) From 4f77c6407b22b9606ea903171ff7e7d85cf70de6 Mon Sep 17 00:00:00 2001 From: Roderick Dunn Date: Mon, 13 Feb 2023 09:42:26 -0500 Subject: [PATCH 3/6] Fixed unit test --- tests/test_query.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_query.py b/tests/test_query.py index 7fae5ce..1420189 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -207,7 +207,7 @@ def test_select_with_optimizer_hints(self): self.assertEqual(q, "SELECT /*+ PARALLEL(a 16) */ b FROM (SELECT * FROM a LIMIT 10) tmp1") q = c.compile(t.select(this.a).group_by(this.b).agg(this.c).select(optimizer_hints='PARALLEL(a 16)')) - self.assertEqual(q, "SELECT /*+ PARALLEL(a 16) */ * FROM (SELECT b, c FROM (SELECT a FROM point) tmp1 GROUP BY 1) tmp2") + self.assertEqual(q, "SELECT /*+ PARALLEL(a 16) */ * FROM (SELECT b, c FROM (SELECT a FROM a) tmp2 GROUP BY 1) tmp3") def test_table_ops(self): c = Compiler(MockDatabase()) From beba98694a0dd73965c3b22da10fee25c8b2abf2 Mon Sep 17 00:00:00 2001 From: Roderick Dunn Date: Mon, 13 Feb 2023 09:43:13 -0500 Subject: [PATCH 4/6] Fixed NotImplimentedError for other DBs --- sqeleton/databases/base.py | 2 +- sqeleton/queries/ast_classes.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sqeleton/databases/base.py b/sqeleton/databases/base.py index f5687d7..f86bdd3 100644 --- a/sqeleton/databases/base.py +++ b/sqeleton/databases/base.py @@ -169,7 +169,7 @@ def explain_as_text(self, query: str) -> str: return f"EXPLAIN {query}" def optimizer_hints(self, s: str): - return f"/*+ {s} */ " if s else "" + return f"/*+ {s} */ " def _constant_value(self, v): if v is None: diff --git a/sqeleton/queries/ast_classes.py b/sqeleton/queries/ast_classes.py index ee27a12..5f676f7 100644 --- a/sqeleton/queries/ast_classes.py +++ b/sqeleton/queries/ast_classes.py @@ -700,7 +700,7 @@ def compile(self, parent_c: Compiler) -> str: columns = ", ".join(map(c.compile, self.columns)) if self.columns else "*" distinct = "DISTINCT " if self.distinct else "" - optimizer_hints = c.dialect.optimizer_hints(self.optimizer_hints) + optimizer_hints = c.dialect.optimizer_hints(self.optimizer_hints) if self.optimizer_hints else "" select = f"SELECT {optimizer_hints}{distinct}{columns}" if self.table: From 21f25376c7e4f33f5beb039c3fba975041c2c340 Mon Sep 17 00:00:00 2001 From: Roderick Dunn Date: Mon, 13 Feb 2023 12:56:12 -0500 Subject: [PATCH 5/6] Base optimizer_hints throws NotImplementedError --- sqeleton/databases/base.py | 4 ++-- sqeleton/databases/bigquery.py | 3 --- sqeleton/databases/clickhouse.py | 3 --- sqeleton/databases/databricks.py | 2 -- sqeleton/databases/duckdb.py | 3 --- sqeleton/databases/mysql.py | 3 +++ sqeleton/databases/oracle.py | 3 +++ sqeleton/databases/postgresql.py | 3 --- sqeleton/databases/presto.py | 3 --- sqeleton/databases/redshift.py | 2 -- sqeleton/databases/vertica.py | 3 --- tests/test_query.py | 2 +- 12 files changed, 9 insertions(+), 25 deletions(-) diff --git a/sqeleton/databases/base.py b/sqeleton/databases/base.py index f86bdd3..643a9c6 100644 --- a/sqeleton/databases/base.py +++ b/sqeleton/databases/base.py @@ -168,8 +168,8 @@ def current_timestamp(self) -> str: def explain_as_text(self, query: str) -> str: return f"EXPLAIN {query}" - def optimizer_hints(self, s: str): - return f"/*+ {s} */ " + def optimizer_hints(self, hints: str) -> str: + raise NotImplementedError(f"Optimizer hints not yet implemented in {self.__class__}") def _constant_value(self, v): if v is None: diff --git a/sqeleton/databases/bigquery.py b/sqeleton/databases/bigquery.py index e9c2c56..0b4dc66 100644 --- a/sqeleton/databases/bigquery.py +++ b/sqeleton/databases/bigquery.py @@ -137,9 +137,6 @@ def type_repr(self, t) -> str: def set_timezone_to_utc(self) -> str: raise NotImplementedError() - def optimizer_hints(self, hints: str) -> str: - raise NotImplementedError("Optimizer hints not yet implemented in bigquery") - class BigQuery(Database): CONNECT_URI_HELP = "bigquery:///" diff --git a/sqeleton/databases/clickhouse.py b/sqeleton/databases/clickhouse.py index 0d40eea..6854b07 100644 --- a/sqeleton/databases/clickhouse.py +++ b/sqeleton/databases/clickhouse.py @@ -162,9 +162,6 @@ def set_timezone_to_utc(self) -> str: def current_timestamp(self) -> str: return "now()" - def optimizer_hints(self, hints: str) -> str: - raise NotImplementedError("Optimizer hints not yet implemented in clickhouse") - class Clickhouse(ThreadedDatabase): dialect = Dialect() diff --git a/sqeleton/databases/databricks.py b/sqeleton/databases/databricks.py index f33a1fc..585a418 100644 --- a/sqeleton/databases/databricks.py +++ b/sqeleton/databases/databricks.py @@ -94,8 +94,6 @@ def _convert_db_precision_to_digits(self, p: int) -> int: def set_timezone_to_utc(self) -> str: return "SET TIME ZONE 'UTC'" - def optimizer_hints(self, hints: str) -> str: - raise NotImplementedError("Optimizer hints not yet implemented in databricks") class Databricks(ThreadedDatabase): diff --git a/sqeleton/databases/duckdb.py b/sqeleton/databases/duckdb.py index a4a2475..07ae4f8 100644 --- a/sqeleton/databases/duckdb.py +++ b/sqeleton/databases/duckdb.py @@ -136,9 +136,6 @@ def set_timezone_to_utc(self) -> str: def current_timestamp(self) -> str: return "current_timestamp" - def optimizer_hints(self, hints: str) -> str: - raise NotImplementedError("Optimizer hints not yet implemented in duckdb") - class DuckDB(Database): dialect = Dialect() diff --git a/sqeleton/databases/mysql.py b/sqeleton/databases/mysql.py index 527282b..0eeab8a 100644 --- a/sqeleton/databases/mysql.py +++ b/sqeleton/databases/mysql.py @@ -109,6 +109,9 @@ def type_repr(self, t) -> str: def explain_as_text(self, query: str) -> str: return f"EXPLAIN FORMAT=TREE {query}" + def optimizer_hints(self, s: str): + return f"/*+ {s} */ " + def set_timezone_to_utc(self) -> str: return "SET @@session.time_zone='+00:00'" diff --git a/sqeleton/databases/oracle.py b/sqeleton/databases/oracle.py index b3da12a..93d88c2 100644 --- a/sqeleton/databases/oracle.py +++ b/sqeleton/databases/oracle.py @@ -130,6 +130,9 @@ def constant_values(self, rows) -> str: def explain_as_text(self, query: str) -> str: raise NotImplementedError("Explain not yet implemented in Oracle") + def optimizer_hints(self, s: str): + return f"/*+ {s} */ " + def parse_type( self, table_path: DbPath, diff --git a/sqeleton/databases/postgresql.py b/sqeleton/databases/postgresql.py index 341e8d1..7c7b47d 100644 --- a/sqeleton/databases/postgresql.py +++ b/sqeleton/databases/postgresql.py @@ -96,9 +96,6 @@ def set_timezone_to_utc(self) -> str: def current_timestamp(self) -> str: return "current_timestamp" - def optimizer_hints(self, hints: str) -> str: - raise NotImplementedError("Optimizer hints not yet implemented in postgresql") - class PostgreSQL(ThreadedDatabase): dialect = PostgresqlDialect() diff --git a/sqeleton/databases/presto.py b/sqeleton/databases/presto.py index 7058798..3dde7db 100644 --- a/sqeleton/databases/presto.py +++ b/sqeleton/databases/presto.py @@ -141,9 +141,6 @@ def set_timezone_to_utc(self) -> str: def current_timestamp(self) -> str: return "current_timestamp" - def optimizer_hints(self, hints: str) -> str: - raise NotImplementedError("Optimizer hints not yet implemented in presto") - class Presto(Database): dialect = Dialect() diff --git a/sqeleton/databases/redshift.py b/sqeleton/databases/redshift.py index 1e05295..b1e6617 100644 --- a/sqeleton/databases/redshift.py +++ b/sqeleton/databases/redshift.py @@ -57,8 +57,6 @@ def concat(self, items: List[str]) -> str: def is_distinct_from(self, a: str, b: str) -> str: return f"({a} IS NULL != {b} IS NULL) OR ({a}!={b})" - def optimizer_hints(self, hints: str) -> str: - raise NotImplementedError("Optimizer hints not yet implemented in redshift") class Redshift(PostgreSQL): dialect = Dialect() diff --git a/sqeleton/databases/vertica.py b/sqeleton/databases/vertica.py index 820b170..3f853ea 100644 --- a/sqeleton/databases/vertica.py +++ b/sqeleton/databases/vertica.py @@ -149,9 +149,6 @@ def set_timezone_to_utc(self) -> str: def current_timestamp(self) -> str: return "current_timestamp(6)" - def optimizer_hints(self, hints: str) -> str: - raise NotImplementedError("Optimizer hints not yet implemented in vertica") - class Vertica(ThreadedDatabase): dialect = Dialect() diff --git a/tests/test_query.py b/tests/test_query.py index 1420189..09469cd 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -52,7 +52,7 @@ def set_timezone_to_utc(self) -> str: return "set timezone 'UTC'" def optimizer_hints(self, s: str): - return f"/*+ {s} */ " if s else "" + return f"/*+ {s} */ " def load_mixins(self): raise NotImplementedError() From 7457caa74ce30104f942af67252c57945eb23af2 Mon Sep 17 00:00:00 2001 From: Roderick Dunn Date: Mon, 13 Feb 2023 13:54:50 -0500 Subject: [PATCH 6/6] Using mixin for optimizer_hints support --- docs/intro.md | 2 ++ sqeleton/abcs/database_types.py | 4 ---- sqeleton/abcs/mixins.py | 12 ++++++++++++ sqeleton/databases/base.py | 17 ++++++++++++++--- sqeleton/databases/mysql.py | 4 ++-- sqeleton/databases/oracle.py | 7 ++----- 6 files changed, 32 insertions(+), 14 deletions(-) diff --git a/docs/intro.md b/docs/intro.md index 25dddf9..f703f9b 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -463,6 +463,8 @@ List of available abstract mixins: - `AbstractMixin_TimeTravel` - Only snowflake & bigquery +- `AbstractMixin_OptimizerHints` - Only oracle & mysql + More will be added in the future. Note that it's still possible to use user-defined mixins that aren't on this list. diff --git a/sqeleton/abcs/database_types.py b/sqeleton/abcs/database_types.py index 77c269a..bc022b5 100644 --- a/sqeleton/abcs/database_types.py +++ b/sqeleton/abcs/database_types.py @@ -201,10 +201,6 @@ def offset_limit(self, offset: Optional[int] = None, limit: Optional[int] = None def explain_as_text(self, query: str) -> str: "Provide SQL for explaining a query, returned as table(varchar)" - @abstractmethod - def optimizer_hints(self, hints: str) -> str: - "Provide SQL for enclosing optimizer hints" - @abstractmethod def timestamp_value(self, t: datetime) -> str: "Provide SQL for the given timestamp value" diff --git a/sqeleton/abcs/mixins.py b/sqeleton/abcs/mixins.py index 34d3250..bf1f277 100644 --- a/sqeleton/abcs/mixins.py +++ b/sqeleton/abcs/mixins.py @@ -145,3 +145,15 @@ def time_travel( Must specify exactly one of `timestamp`, `offset` or `statement`. """ + +class AbstractMixin_OptimizerHints(AbstractMixin): + @abstractmethod + def optimizer_hints( + self, + optimizer_hints: str + ) -> str: + """Creates a compatible optimizer_hints string + + Parameters: + optimizer_hints - string of opimizer hints + """ \ No newline at end of file diff --git a/sqeleton/databases/base.py b/sqeleton/databases/base.py index 643a9c6..9d1b6c3 100644 --- a/sqeleton/databases/base.py +++ b/sqeleton/databases/base.py @@ -36,7 +36,12 @@ Boolean, ) from ..abcs.mixins import Compilable -from ..abcs.mixins import AbstractMixin_Schema, AbstractMixin_RandomSample, AbstractMixin_NormalizeValue +from ..abcs.mixins import ( + AbstractMixin_Schema, + AbstractMixin_RandomSample, + AbstractMixin_NormalizeValue, + AbstractMixin_OptimizerHints +) from ..bound_exprs import bound_table logger = logging.getLogger("database") @@ -134,6 +139,14 @@ def random_sample_ratio_approx(self, tbl: AbstractTable, ratio: float) -> Abstra return tbl.where(Random() < ratio) +class Mixin_OptimizerHints(AbstractMixin_OptimizerHints): + def optimizer_hints( + self, + hints: str + ) -> str: + return f"/*+ {hints} */ " + + class BaseDialect(AbstractDialect): SUPPORTS_PRIMARY_KEY = False SUPPORTS_INDEXES = False @@ -168,8 +181,6 @@ def current_timestamp(self) -> str: def explain_as_text(self, query: str) -> str: return f"EXPLAIN {query}" - def optimizer_hints(self, hints: str) -> str: - raise NotImplementedError(f"Optimizer hints not yet implemented in {self.__class__}") def _constant_value(self, v): if v is None: diff --git a/sqeleton/databases/mysql.py b/sqeleton/databases/mysql.py index 0eeab8a..fd4bc29 100644 --- a/sqeleton/databases/mysql.py +++ b/sqeleton/databases/mysql.py @@ -17,7 +17,7 @@ AbstractMixin_Regex, AbstractMixin_RandomSample, ) -from .base import ThreadedDatabase, import_helper, ConnectError, BaseDialect, Compilable +from .base import Mixin_OptimizerHints, ThreadedDatabase, import_helper, ConnectError, BaseDialect, Compilable from .base import MD5_HEXDIGITS, CHECKSUM_HEXDIGITS, TIMESTAMP_PRECISION_POS, Mixin_Schema, Mixin_RandomSample from ..queries.ast_classes import BinBoolOp @@ -54,7 +54,7 @@ def test_regex(self, string: Compilable, pattern: Compilable) -> Compilable: return BinBoolOp("REGEXP", [string, pattern]) -class Dialect(BaseDialect, Mixin_Schema): +class Dialect(BaseDialect, Mixin_Schema, Mixin_OptimizerHints): name = "MySQL" ROUNDS_ON_PREC_LOSS = True SUPPORTS_PRIMARY_KEY = True diff --git a/sqeleton/databases/oracle.py b/sqeleton/databases/oracle.py index 93d88c2..79b5ec3 100644 --- a/sqeleton/databases/oracle.py +++ b/sqeleton/databases/oracle.py @@ -17,7 +17,7 @@ from ..abcs.mixins import AbstractMixin_MD5, AbstractMixin_NormalizeValue, AbstractMixin_Schema from ..abcs import Compilable from ..queries import this, table, SKIP -from .base import BaseDialect, ThreadedDatabase, import_helper, ConnectError, QueryError, Mixin_RandomSample +from .base import BaseDialect, Mixin_OptimizerHints, ThreadedDatabase, import_helper, ConnectError, QueryError, Mixin_RandomSample from .base import TIMESTAMP_PRECISION_POS SESSION_TIME_ZONE = None # Changed by the tests @@ -72,7 +72,7 @@ def list_tables(self, table_schema: str, like: Compilable = None) -> Compilable: ) -class Dialect(BaseDialect, Mixin_Schema): +class Dialect(BaseDialect, Mixin_Schema, Mixin_OptimizerHints): name = "Oracle" SUPPORTS_PRIMARY_KEY = True SUPPORTS_INDEXES = True @@ -130,9 +130,6 @@ def constant_values(self, rows) -> str: def explain_as_text(self, query: str) -> str: raise NotImplementedError("Explain not yet implemented in Oracle") - def optimizer_hints(self, s: str): - return f"/*+ {s} */ " - def parse_type( self, table_path: DbPath,