diff --git a/changelog.d/24.fixed.md b/changelog.d/24.fixed.md new file mode 100644 index 00000000..a4688bde --- /dev/null +++ b/changelog.d/24.fixed.md @@ -0,0 +1 @@ +Refactored SQL function generation utilities by migrating them from types/ to functions/ directory for better module responsibility separation. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/__init__.py index f53168c0..317403e1 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/__init__.py @@ -60,6 +60,8 @@ from .types.text_search import ( PostgresTsVector, PostgresTsQuery, +) +from .functions.text_search import ( to_tsvector, to_tsquery, plainto_tsquery, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/__init__.py index dc647f92..2b3a366f 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/__init__.py @@ -18,6 +18,8 @@ - enum: Enum functions - https://www.postgresql.org/docs/current/functions-enum.html - bit_string: Bit string functions and operators - https://www.postgresql.org/docs/current/functions-bitstring.html - json: JSON functions and operators - https://www.postgresql.org/docs/current/functions-json.html +- text_search: Full-text search functions - https://www.postgresql.org/docs/current/functions-textsearch.html +- xml: XML functions - https://www.postgresql.org/docs/current/functions-xml.html """ # Range functions @@ -102,6 +104,32 @@ jsonb_path_match, ) +# Text search functions +from .text_search import ( + to_tsvector, + to_tsquery, + plainto_tsquery, + phraseto_tsquery, + websearch_to_tsquery, + ts_matches, + ts_matches_expr, + ts_rank, + ts_rank_cd, + ts_headline, + tsvector_concat, + tsvector_strip, + tsvector_setweight, + tsvector_length, +) + +# XML functions +from .xml import ( + xmlparse, + xpath_query, + xpath_exists, + xml_is_well_formed, +) + __all__ = [ # Range functions @@ -171,4 +199,24 @@ "jsonb_path_query_first", "jsonb_path_exists", "jsonb_path_match", + # Text search functions + "to_tsvector", + "to_tsquery", + "plainto_tsquery", + "phraseto_tsquery", + "websearch_to_tsquery", + "ts_matches", + "ts_matches_expr", + "ts_rank", + "ts_rank_cd", + "ts_headline", + "tsvector_concat", + "tsvector_strip", + "tsvector_setweight", + "tsvector_length", + # XML functions + "xmlparse", + "xpath_query", + "xpath_exists", + "xml_is_well_formed", ] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/text_search.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/text_search.py new file mode 100644 index 00000000..af04ecb0 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/text_search.py @@ -0,0 +1,375 @@ +# src/rhosocial/activerecord/backend/impl/postgres/functions/text_search.py +""" +PostgreSQL text search functions for SQL expression generation. + +This module provides utility functions for generating PostgreSQL full-text search SQL expressions. + +Supported functions: +- to_tsvector() - Convert text to tsvector +- to_tsquery() - Convert to tsquery +- plainto_tsquery() - Convert plain text to tsquery +- phraseto_tsquery() - Convert phrase text to tsquery +- websearch_to_tsquery() - Convert web search style query to tsquery +- ts_matches() - Test tsvector @@ tsquery +- ts_matches_expr() - Test tsvector @@@ tsquery (deprecated) +- ts_rank() - Calculate relevance rank +- ts_rank_cd() - Calculate rank by cover density +- ts_headline() - Display query matches with highlighting +- tsvector_concat() - Concatenate two tsvectors +- tsvector_strip() - Remove positions and weights from tsvector +- tsvector_setweight() - Set weight for all lexemes +- tsvector_length() - Get number of lexemes + +Examples: + Generate tsvector from document: + >>> to_tsvector("hello world", config="english") + "to_tsvector('english', 'hello world')" + + Generate tsquery from query: + >>> to_tsquery("hello & world", config="english") + "to_tsquery('english', 'hello & world')" + + Test if tsvector matches tsquery: + >>> ts_matches("title_tsvector", "to_tsquery('hello')") + "title_tsvector @@ to_tsquery('hello')" + + Calculate relevance rank: + >>> ts_rank("title_tsvector", "to_tsquery('hello')") + "ts_rank(title_tsvector, to_tsquery('hello'))" + + Get highlighted text with matches: + >>> ts_headline("content", "to_tsquery('hello')") + "ts_headline(content, to_tsquery('hello'))" +""" + +from typing import List, Optional + + +def to_tsvector(document: str, config: Optional[str] = None) -> str: + """Generate SQL expression for PostgreSQL to_tsvector function. + + Args: + document: The document text to convert to tsvector + config: Optional text search configuration (e.g., 'english', 'simple') + + Returns: + str: SQL expression for to_tsvector function + + Examples: + >>> to_tsvector("hello world") + "to_tsvector('hello world')" + >>> to_tsvector("hello world", "english") + "to_tsvector('english', 'hello world')" + """ + if config: + return f"to_tsvector('{config}', {document})" + return f"to_tsvector({document})" + + +def to_tsquery(query: str, config: Optional[str] = None) -> str: + """Generate SQL expression for PostgreSQL to_tsquery function. + + Args: + query: The query string to convert to tsquery + config: Optional text search configuration + + Returns: + str: SQL expression for to_tsquery function + + Examples: + >>> to_tsquery("hello & world") + "to_tsquery('hello & world')" + >>> to_tsquery("hello & world", "english") + "to_tsquery('english', 'hello & world')" + """ + if config: + return f"to_tsquery('{config}', '{query}')" + return f"to_tsquery('{query}')" + + +def plainto_tsquery(query: str, config: Optional[str] = None) -> str: + """Generate SQL expression for PostgreSQL plainto_tsquery function. + + Converts plain text to tsquery, handling special characters. + + Args: + query: The plain text query + config: Optional text search configuration + + Returns: + str: SQL expression for plainto_tsquery function + + Examples: + >>> plainto_tsquery("hello world") + "plainto_tsquery('hello world')" + >>> plainto_tsquery("hello world", "english") + "plainto_tsquery('english', 'hello world')" + """ + if config: + return f"plainto_tsquery('{config}', '{query}')" + return f"plainto_tsquery('{query}')" + + +def phraseto_tsquery(query: str, config: Optional[str] = None) -> str: + """Generate SQL expression for PostgreSQL phraseto_tsquery function. + + Converts text to tsquery for phrase search. + + Args: + query: The phrase query string + config: Optional text search configuration + + Returns: + str: SQL expression for phraseto_tsquery function + + Examples: + >>> phraseto_tsquery("hello world") + "phraseto_tsquery('hello world')" + >>> phraseto_tsquery("hello world", "english") + "phraseto_tsquery('english', 'hello world')" + """ + if config: + return f"phraseto_tsquery('{config}', '{query}')" + return f"phraseto_tsquery('{query}')" + + +def websearch_to_tsquery(query: str, config: Optional[str] = None) -> str: + """Generate SQL expression for PostgreSQL websearch_to_tsquery function. + + Converts web search style query to tsquery (PostgreSQL 11+). + + Args: + query: The web search query string + config: Optional text search configuration + + Returns: + str: SQL expression for websearch_to_tsquery function + + Examples: + >>> websearch_to_tsquery("hello -world") + "websearch_to_tsquery('hello -world')" + >>> websearch_to_tsquery("hello -world", "english") + "websearch_to_tsquery('english', 'hello -world')" + """ + if config: + return f"websearch_to_tsquery('{config}', '{query}')" + return f"websearch_to_tsquery('{query}')" + + +def ts_matches(vector: str, query: str) -> str: + """Generate SQL expression for tsvector @@ tsquery match operator. + + Args: + vector: The tsvector expression or column + query: The tsquery expression + + Returns: + str: SQL expression using @@ operator + + Examples: + >>> ts_matches("title_tsvector", "to_tsquery('hello')") + "title_tsvector @@ to_tsquery('hello')" + """ + return f"{vector} @@ {query}" + + +def ts_matches_expr(vector: str, query: str) -> str: + """Generate SQL expression for tsvector @@@ tsquery match operator. + + The @@@ operator is a deprecated alias for @@. + + Args: + vector: The tsvector expression or column + query: The tsquery expression + + Returns: + str: SQL expression using @@@ operator + + Examples: + >>> ts_matches_expr("title_tsvector", "to_tsquery('hello')") + "title_tsvector @@@ to_tsquery('hello')" + """ + return f"{vector} @@@ {query}" + + +def ts_rank(vector: str, query: str, weights: Optional[List[float]] = None, normalization: int = 0) -> str: + """Generate SQL expression for PostgreSQL ts_rank function. + + Calculates relevance rank for tsvector matching tsquery. + + Args: + vector: The tsvector expression or column + query: The tsquery expression + weights: Optional list of 4 weights [D, C, B, A] (default [0.1, 0.2, 0.4, 1.0]) + normalization: Normalization method (0-32, default 0) + + Returns: + str: SQL expression for ts_rank function + + Examples: + >>> ts_rank("title_tsvector", "to_tsquery('hello')") + "ts_rank(title_tsvector, to_tsquery('hello'))" + >>> ts_rank("title_tsvector", "to_tsquery('hello')", [0.1, 0.2, 0.4, 1.0]) + "ts_rank(array[0.1, 0.2, 0.4, 1.0], title_tsvector, to_tsquery('hello'))" + >>> ts_rank("title_tsvector", "to_tsquery('hello')", normalization=1) + "ts_rank(title_tsvector, to_tsquery('hello'), 1)" + """ + if weights: + weights_str = f"array[{', '.join(str(w) for w in weights)}]" + if normalization != 0: + return f"ts_rank({weights_str}, {vector}, {query}, {normalization})" + return f"ts_rank({weights_str}, {vector}, {query})" + if normalization != 0: + return f"ts_rank({vector}, {query}, {normalization})" + return f"ts_rank({vector}, {query})" + + +def ts_rank_cd(vector: str, query: str, weights: Optional[List[float]] = None, normalization: int = 0) -> str: + """Generate SQL expression for PostgreSQL ts_rank_cd function. + + Calculates relevance rank using cover density. + + Args: + vector: The tsvector expression or column + query: The tsquery expression + weights: Optional list of 4 weights [D, C, B, A] + normalization: Normalization method (0-32, default 0) + + Returns: + str: SQL expression for ts_rank_cd function + + Examples: + >>> ts_rank_cd("title_tsvector", "to_tsquery('hello')") + "ts_rank_cd(title_tsvector, to_tsquery('hello'))" + >>> ts_rank_cd("title_tsvector", "to_tsquery('hello')", [0.1, 0.2, 0.4, 1.0]) + "ts_rank_cd(array[0.1, 0.2, 0.4, 1.0], title_tsvector, to_tsquery('hello'))" + """ + if weights: + weights_str = f"array[{', '.join(str(w) for w in weights)}]" + if normalization != 0: + return f"ts_rank_cd({weights_str}, {vector}, {query}, {normalization})" + return f"ts_rank_cd({weights_str}, {vector}, {query})" + if normalization != 0: + return f"ts_rank_cd({vector}, {query}, {normalization})" + return f"ts_rank_cd({vector}, {query})" + + +def ts_headline(document: str, query: str, config: Optional[str] = None, options: Optional[str] = None) -> str: + """Generate SQL expression for PostgreSQL ts_headline function. + + Displays query matches in document with highlighting. + + Args: + document: The document text or expression + query: The tsquery expression + config: Optional text search configuration + options: Optional headline options string + + Returns: + str: SQL expression for ts_headline function + + Examples: + >>> ts_headline("content", "to_tsquery('hello')") + "ts_headline(content, to_tsquery('hello'))" + >>> ts_headline("content", "to_tsquery('hello')", config="english") + "ts_headline('english', content, to_tsquery('hello'))" + >>> ts_headline("content", "to_tsquery('hello')", options="StartSel=, StopSel=") + "ts_headline(content, to_tsquery('hello'), 'StartSel=, StopSel=')" + """ + if config: + if options: + return f"ts_headline('{config}', {document}, {query}, '{options}')" + return f"ts_headline('{config}', {document}, {query})" + if options: + return f"ts_headline({document}, {query}, '{options}')" + return f"ts_headline({document}, {query})" + + +def tsvector_concat(vec1: str, vec2: str) -> str: + """Generate SQL expression for tsvector concatenation. + + Args: + vec1: First tsvector expression + vec2: Second tsvector expression + + Returns: + str: SQL expression using || operator + + Examples: + >>> tsvector_concat("title_tsvector", "body_tsvector") + "title_tsvector || body_tsvector" + """ + return f"{vec1} || {vec2}" + + +def tsvector_strip(vec: str) -> str: + """Generate SQL expression for strip function. + + Removes positions and weights from tsvector. + + Args: + vec: The tsvector expression + + Returns: + str: SQL expression for strip function + + Examples: + >>> tsvector_strip("title_tsvector") + "strip(title_tsvector)" + """ + return f"strip({vec})" + + +def tsvector_setweight(vec: str, weight: str) -> str: + """Generate SQL expression for setweight function. + + Sets weight for all lexemes in tsvector. + + Args: + vec: The tsvector expression + weight: Weight character ('A', 'B', 'C', or 'D') + + Returns: + str: SQL expression for setweight function + + Examples: + >>> tsvector_setweight("title_tsvector", "A") + "setweight(title_tsvector, 'A')" + """ + return f"setweight({vec}, '{weight}')" + + +def tsvector_length(vec: str) -> str: + """Generate SQL expression for tsvector length. + + Returns the number of lexemes in tsvector. + + Args: + vec: The tsvector expression + + Returns: + str: SQL expression for length function + + Examples: + >>> tsvector_length("title_tsvector") + "length(title_tsvector)" + """ + return f"length({vec})" + + +__all__ = [ + "to_tsvector", + "to_tsquery", + "plainto_tsquery", + "phraseto_tsquery", + "websearch_to_tsquery", + "ts_matches", + "ts_matches_expr", + "ts_rank", + "ts_rank_cd", + "ts_headline", + "tsvector_concat", + "tsvector_strip", + "tsvector_setweight", + "tsvector_length", +] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/xml.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/xml.py new file mode 100644 index 00000000..7a33d357 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/xml.py @@ -0,0 +1,116 @@ +# src/rhosocial/activerecord/backend/impl/postgres/functions/xml.py +""" +PostgreSQL XML functions for SQL expression generation. + +This module provides utility functions for generating PostgreSQL XML-related SQL expressions. + +Supported functions: +- xmlparse() - Parse XML content +- xpath_query() - Execute XPath query on XML +- xpath_exists() - Test if XPath expression matches +- xml_is_well_formed() - Check if XML is well-formed + +Examples: + Parse XML document: + >>> xmlparse('value') + "XMLPARSE(DOCUMENT 'value')" + + Query XML with XPath: + >>> xpath_query("xml_column", "/root/item") + "xpath('/root/item', xml_column)" + + Test if XPath exists: + >>> xpath_exists("xml_column", "/root/item[@id='1']") + "xpath_exists('/root/item[@id='1']', xml_column)" + + Check if well-formed: + >>> xml_is_well_formed('') + "xml_is_well_formed('')" +""" + +from typing import Dict, Optional, Union + +# Import type for annotations +from ..types.xml import PostgresXML + + +def xmlparse(content: str, document: bool = True, preserve_whitespace: bool = False) -> str: + """Generate XMLPARSE expression. + + Args: + content: XML content string + document: Parse as DOCUMENT (default) or CONTENT + preserve_whitespace: Preserve whitespace + + Returns: + SQL expression for XMLPARSE + """ + doc_type = "DOCUMENT" if document else "CONTENT" + whitespace = "PRESERVE WHITESPACE" if preserve_whitespace else "" + return f"XMLPARSE({doc_type} '{content}' {whitespace})".strip() + + +def xpath_query(xml_value: Union[str, PostgresXML], xpath: str, namespaces: Optional[Dict[str, str]] = None) -> str: + """Generate XPath query expression. + + Args: + xml_value: XML value or column reference + xpath: XPath expression + namespaces: Optional namespace mappings + + Returns: + SQL expression for xpath function + """ + if isinstance(xml_value, PostgresXML): + xml_value = f"'{xml_value.content}'" + + if namespaces: + ns_array = ", ".join(f"ARRAY[{k!s}, {v!s}]" for k, v in namespaces.items()) + ns_param = f", ARRAY[{ns_array}]" + else: + ns_param = "" + + return f"xpath('{xpath}', {xml_value}{ns_param})" + + +def xpath_exists(xml_value: Union[str, PostgresXML], xpath: str, namespaces: Optional[Dict[str, str]] = None) -> str: + """Generate xpath_exists expression. + + Args: + xml_value: XML value or column reference + xpath: XPath expression + namespaces: Optional namespace mappings + + Returns: + SQL expression for xpath_exists function + """ + if isinstance(xml_value, PostgresXML): + xml_value = f"'{xml_value.content}'" + + if namespaces: + ns_array = ", ".join(f"ARRAY[{k!s}, {v!s}]" for k, v in namespaces.items()) + ns_param = f", ARRAY[{ns_array}]" + else: + ns_param = "" + + return f"xpath_exists('{xpath}', {xml_value}{ns_param})" + + +def xml_is_well_formed(content: str) -> str: + """Generate xml_is_well_formed expression (PostgreSQL 9.1+). + + Args: + content: XML content to check + + Returns: + SQL expression for xml_is_well_formed function + """ + return f"xml_is_well_formed('{content}')" + + +__all__ = [ + "xmlparse", + "xpath_query", + "xpath_exists", + "xml_is_well_formed", +] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/types/__init__.py index 1782da82..7f49ada8 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/types/__init__.py @@ -100,10 +100,6 @@ # XML type from .xml import ( PostgresXML, - xmlparse, - xpath_query, - xpath_exists, - xml_is_well_formed, ) # Type constants (convenience references) @@ -260,10 +256,6 @@ "PostgresMoney", # XML type "PostgresXML", - "xmlparse", - "xpath_query", - "xpath_exists", - "xml_is_well_formed", # Type constants - Numeric "SMALLINT", "INTEGER", diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/text_search.py b/src/rhosocial/activerecord/backend/impl/postgres/types/text_search.py index 5be8d924..da10b5e1 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/text_search.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/types/text_search.py @@ -747,321 +747,6 @@ def _skip_whitespace(self) -> None: self.pos += 1 -# Text search utility functions for SQL generation - - -def to_tsvector(document: str, config: Optional[str] = None) -> str: - """Generate SQL expression for PostgreSQL to_tsvector function. - - Args: - document: The document text to convert to tsvector - config: Optional text search configuration (e.g., 'english', 'simple') - - Returns: - str: SQL expression for to_tsvector function - - Examples: - >>> to_tsvector("hello world") - "to_tsvector('hello world')" - >>> to_tsvector("hello world", "english") - "to_tsvector('english', 'hello world')" - """ - if config: - return f"to_tsvector('{config}', {document})" - return f"to_tsvector({document})" - - -def to_tsquery(query: str, config: Optional[str] = None) -> str: - """Generate SQL expression for PostgreSQL to_tsquery function. - - Args: - query: The query string to convert to tsquery - config: Optional text search configuration - - Returns: - str: SQL expression for to_tsquery function - - Examples: - >>> to_tsquery("hello & world") - "to_tsquery('hello & world')" - >>> to_tsquery("hello & world", "english") - "to_tsquery('english', 'hello & world')" - """ - if config: - return f"to_tsquery('{config}', '{query}')" - return f"to_tsquery('{query}')" - - -def plainto_tsquery(query: str, config: Optional[str] = None) -> str: - """Generate SQL expression for PostgreSQL plainto_tsquery function. - - Converts plain text to tsquery, handling special characters. - - Args: - query: The plain text query - config: Optional text search configuration - - Returns: - str: SQL expression for plainto_tsquery function - - Examples: - >>> plainto_tsquery("hello world") - "plainto_tsquery('hello world')" - >>> plainto_tsquery("hello world", "english") - "plainto_tsquery('english', 'hello world')" - """ - if config: - return f"plainto_tsquery('{config}', '{query}')" - return f"plainto_tsquery('{query}')" - - -def phraseto_tsquery(query: str, config: Optional[str] = None) -> str: - """Generate SQL expression for PostgreSQL phraseto_tsquery function. - - Converts text to tsquery for phrase search. - - Args: - query: The phrase query string - config: Optional text search configuration - - Returns: - str: SQL expression for phraseto_tsquery function - - Examples: - >>> phraseto_tsquery("hello world") - "phraseto_tsquery('hello world')" - >>> phraseto_tsquery("hello world", "english") - "phraseto_tsquery('english', 'hello world')" - """ - if config: - return f"phraseto_tsquery('{config}', '{query}')" - return f"phraseto_tsquery('{query}')" - - -def websearch_to_tsquery(query: str, config: Optional[str] = None) -> str: - """Generate SQL expression for PostgreSQL websearch_to_tsquery function. - - Converts web search style query to tsquery (PostgreSQL 11+). - - Args: - query: The web search query string - config: Optional text search configuration - - Returns: - str: SQL expression for websearch_to_tsquery function - - Examples: - >>> websearch_to_tsquery("hello -world") - "websearch_to_tsquery('hello -world')" - >>> websearch_to_tsquery("hello -world", "english") - "websearch_to_tsquery('english', 'hello -world')" - """ - if config: - return f"websearch_to_tsquery('{config}', '{query}')" - return f"websearch_to_tsquery('{query}')" - - -def ts_matches(vector: str, query: str) -> str: - """Generate SQL expression for tsvector @@ tsquery match operator. - - Args: - vector: The tsvector expression or column - query: The tsquery expression - - Returns: - str: SQL expression using @@ operator - - Examples: - >>> ts_matches("title_tsvector", "to_tsquery('hello')") - "title_tsvector @@ to_tsquery('hello')" - """ - return f"{vector} @@ {query}" - - -def ts_matches_expr(vector: str, query: str) -> str: - """Generate SQL expression for tsvector @@@ tsquery match operator. - - The @@@ operator is a deprecated alias for @@. - - Args: - vector: The tsvector expression or column - query: The tsquery expression - - Returns: - str: SQL expression using @@@ operator - - Examples: - >>> ts_matches_expr("title_tsvector", "to_tsquery('hello')") - "title_tsvector @@@ to_tsquery('hello')" - """ - return f"{vector} @@@ {query}" - - -def ts_rank(vector: str, query: str, weights: Optional[List[float]] = None, normalization: int = 0) -> str: - """Generate SQL expression for PostgreSQL ts_rank function. - - Calculates relevance rank for tsvector matching tsquery. - - Args: - vector: The tsvector expression or column - query: The tsquery expression - weights: Optional list of 4 weights [D, C, B, A] (default [0.1, 0.2, 0.4, 1.0]) - normalization: Normalization method (0-32, default 0) - - Returns: - str: SQL expression for ts_rank function - - Examples: - >>> ts_rank("title_tsvector", "to_tsquery('hello')") - "ts_rank(title_tsvector, to_tsquery('hello'))" - >>> ts_rank("title_tsvector", "to_tsquery('hello')", [0.1, 0.2, 0.4, 1.0]) - "ts_rank(array[0.1, 0.2, 0.4, 1.0], title_tsvector, to_tsquery('hello'))" - >>> ts_rank("title_tsvector", "to_tsquery('hello')", normalization=1) - "ts_rank(title_tsvector, to_tsquery('hello'), 1)" - """ - if weights: - weights_str = f"array[{', '.join(str(w) for w in weights)}]" - if normalization != 0: - return f"ts_rank({weights_str}, {vector}, {query}, {normalization})" - return f"ts_rank({weights_str}, {vector}, {query})" - if normalization != 0: - return f"ts_rank({vector}, {query}, {normalization})" - return f"ts_rank({vector}, {query})" - - -def ts_rank_cd(vector: str, query: str, weights: Optional[List[float]] = None, normalization: int = 0) -> str: - """Generate SQL expression for PostgreSQL ts_rank_cd function. - - Calculates relevance rank using cover density. - - Args: - vector: The tsvector expression or column - query: The tsquery expression - weights: Optional list of 4 weights [D, C, B, A] - normalization: Normalization method (0-32, default 0) - - Returns: - str: SQL expression for ts_rank_cd function - - Examples: - >>> ts_rank_cd("title_tsvector", "to_tsquery('hello')") - "ts_rank_cd(title_tsvector, to_tsquery('hello'))" - >>> ts_rank_cd("title_tsvector", "to_tsquery('hello')", [0.1, 0.2, 0.4, 1.0]) - "ts_rank_cd(array[0.1, 0.2, 0.4, 1.0], title_tsvector, to_tsquery('hello'))" - """ - if weights: - weights_str = f"array[{', '.join(str(w) for w in weights)}]" - if normalization != 0: - return f"ts_rank_cd({weights_str}, {vector}, {query}, {normalization})" - return f"ts_rank_cd({weights_str}, {vector}, {query})" - if normalization != 0: - return f"ts_rank_cd({vector}, {query}, {normalization})" - return f"ts_rank_cd({vector}, {query})" - - -def ts_headline(document: str, query: str, config: Optional[str] = None, options: Optional[str] = None) -> str: - """Generate SQL expression for PostgreSQL ts_headline function. - - Displays query matches in document with highlighting. - - Args: - document: The document text or expression - query: The tsquery expression - config: Optional text search configuration - options: Optional headline options string - - Returns: - str: SQL expression for ts_headline function - - Examples: - >>> ts_headline("content", "to_tsquery('hello')") - "ts_headline(content, to_tsquery('hello'))" - >>> ts_headline("content", "to_tsquery('hello')", config="english") - "ts_headline('english', content, to_tsquery('hello'))" - >>> ts_headline("content", "to_tsquery('hello')", options="StartSel=, StopSel=") - "ts_headline(content, to_tsquery('hello'), 'StartSel=, StopSel=')" - """ - if config: - if options: - return f"ts_headline('{config}', {document}, {query}, '{options}')" - return f"ts_headline('{config}', {document}, {query})" - if options: - return f"ts_headline({document}, {query}, '{options}')" - return f"ts_headline({document}, {query})" - - -def tsvector_concat(vec1: str, vec2: str) -> str: - """Generate SQL expression for tsvector concatenation. - - Args: - vec1: First tsvector expression - vec2: Second tsvector expression - - Returns: - str: SQL expression using || operator - - Examples: - >>> tsvector_concat("title_tsvector", "body_tsvector") - "title_tsvector || body_tsvector" - """ - return f"{vec1} || {vec2}" - - -def tsvector_strip(vec: str) -> str: - """Generate SQL expression for strip function. - - Removes positions and weights from tsvector. - - Args: - vec: The tsvector expression - - Returns: - str: SQL expression for strip function - - Examples: - >>> tsvector_strip("title_tsvector") - "strip(title_tsvector)" - """ - return f"strip({vec})" - - -def tsvector_setweight(vec: str, weight: str) -> str: - """Generate SQL expression for setweight function. - - Sets weight for all lexemes in tsvector. - - Args: - vec: The tsvector expression - weight: Weight character ('A', 'B', 'C', or 'D') - - Returns: - str: SQL expression for setweight function - - Examples: - >>> tsvector_setweight("title_tsvector", "A") - "setweight(title_tsvector, 'A')" - """ - return f"setweight({vec}, '{weight}')" - - -def tsvector_length(vec: str) -> str: - """Generate SQL expression for tsvector length. - - Returns the number of lexemes in tsvector. - - Args: - vec: The tsvector expression - - Returns: - str: SQL expression for length function - - Examples: - >>> tsvector_length("title_tsvector") - "length(title_tsvector)" - """ - return f"length({vec})" - - __all__ = [ "TsVectorLexeme", "PostgresTsVector", @@ -1069,18 +754,4 @@ def tsvector_length(vec: str) -> str: "TsQueryLexeme", "TsQueryOperator", "PostgresTsQuery", - "to_tsvector", - "to_tsquery", - "plainto_tsquery", - "phraseto_tsquery", - "websearch_to_tsquery", - "ts_matches", - "ts_matches_expr", - "ts_rank", - "ts_rank_cd", - "ts_headline", - "tsvector_concat", - "tsvector_strip", - "tsvector_setweight", - "tsvector_length", ] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/xml.py b/src/rhosocial/activerecord/backend/impl/postgres/types/xml.py index 7fb6c603..6e377196 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/xml.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/types/xml.py @@ -16,7 +16,6 @@ """ from dataclasses import dataclass -from typing import Dict, Optional, Union @dataclass @@ -73,81 +72,4 @@ def is_well_formed(self) -> bool: return True -# XML utility functions for use with PostgreSQL - - -def xmlparse(content: str, document: bool = True, preserve_whitespace: bool = False) -> str: - """Generate XMLPARSE expression. - - Args: - content: XML content string - document: Parse as DOCUMENT (default) or CONTENT - preserve_whitespace: Preserve whitespace - - Returns: - SQL expression for XMLPARSE - """ - doc_type = "DOCUMENT" if document else "CONTENT" - whitespace = "PRESERVE WHITESPACE" if preserve_whitespace else "" - return f"XMLPARSE({doc_type} '{content}' {whitespace})".strip() - - -def xpath_query(xml_value: Union[str, PostgresXML], xpath: str, namespaces: Optional[Dict[str, str]] = None) -> str: - """Generate XPath query expression. - - Args: - xml_value: XML value or column reference - xpath: XPath expression - namespaces: Optional namespace mappings - - Returns: - SQL expression for xpath function - """ - if isinstance(xml_value, PostgresXML): - xml_value = f"'{xml_value.content}'" - - if namespaces: - ns_array = ", ".join(f"ARRAY[{k!s}, {v!s}]" for k, v in namespaces.items()) - ns_param = f", ARRAY[{ns_array}]" - else: - ns_param = "" - - return f"xpath('{xpath}', {xml_value}{ns_param})" - - -def xpath_exists(xml_value: Union[str, PostgresXML], xpath: str, namespaces: Optional[Dict[str, str]] = None) -> str: - """Generate xpath_exists expression. - - Args: - xml_value: XML value or column reference - xpath: XPath expression - namespaces: Optional namespace mappings - - Returns: - SQL expression for xpath_exists function - """ - if isinstance(xml_value, PostgresXML): - xml_value = f"'{xml_value.content}'" - - if namespaces: - ns_array = ", ".join(f"ARRAY[{k!s}, {v!s}]" for k, v in namespaces.items()) - ns_param = f", ARRAY[{ns_array}]" - else: - ns_param = "" - - return f"xpath_exists('{xpath}', {xml_value}{ns_param})" - - -def xml_is_well_formed(content: str) -> str: - """Generate xml_is_well_formed expression (PostgreSQL 9.1+). - - Args: - content: XML content to check - - Returns: - SQL expression for xml_is_well_formed function - """ - return f"xml_is_well_formed('{content}')" - - -__all__ = ["PostgresXML", "xmlparse", "xpath_query", "xpath_exists", "xml_is_well_formed"] +__all__ = ["PostgresXML"] diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_xml_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_xml_types.py index d7f34b85..a7e4760d 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_xml_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_xml_types.py @@ -10,6 +10,8 @@ from rhosocial.activerecord.backend.impl.postgres.types.xml import ( PostgresXML, +) +from rhosocial.activerecord.backend.impl.postgres.functions.xml import ( xmlparse, xpath_query, xpath_exists,