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,