tdf#42713 maintain comments format

If the query has comments then maintain the format when saving
and warn the user when switching or start editing in design view
that the comments could be lost. The user will have the choice
to keep the formating as well as the placement of comments in
the query.

Change-Id: I3de2313f8808a0b1093ce38de3bc9aa775656067
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/207198
Reviewed-by: Vissarion Fisikopoulos <vfisikop@libreoffice.org>
Tested-by: Jenkins
This commit is contained in:
Vissarion Fisikopoulos
2026-06-29 11:37:14 +02:00
parent 1b9b98c6de
commit ef11378a8d
4 changed files with 193 additions and 165 deletions
+3
View File
@@ -139,6 +139,9 @@
#define STR_ERROR_PARSING_STATEMENT NC_("STR_ERROR_PARSING_STATEMENT", "$object$ is based on an SQL command which could not be parsed.")
// To translators: for $object$, one of the values of the RSC_QUERY_OBJECT_TYPE resource (except \"SQL command\", which doesn't make sense here) will be inserted.
#define STR_INFO_OPENING_IN_SQL_VIEW NC_("STR_INFO_OPENING_IN_SQL_VIEW", "$object$ will be opened in SQL view.")
// tdf#42713: warn before switching to Design View when SQL contains comments
// To translators: for $object$, one of the values of the RSC_QUERY_OBJECT_TYPE resource will be inserted.
#define STR_QRY_COMMENTS_WARNING NC_("STR_QRY_COMMENTS_WARNING", "$object$ contains SQL comments.\n\nIf you edit it in Design View, the comments will be lost.\n\nContinue anyway?")
#define STR_STATEMENT_WITHOUT_RESULT_SET NC_("STR_STATEMENT_WITHOUT_RESULT_SET", "The query does not create a result set, and thus cannot be part of another query.")
#define RID_STR_COLUMN_FORMAT NC_("RID_STR_COLUMN_FORMAT", "Column ~Format...")
Binary file not shown.
@@ -7,81 +7,43 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# Regression test for tdf#42713 - SQL comments were lost when switching
# between Design view and SQL view in the Base query editor.
# Regression test for tdf#42713 - SQL comments are preserved when working
# with queries containing comments in the Base query editor.
from uitest.framework import UITestCase
from uitest.uihelper.common import get_url_for_data_file, get_state_as_dict
from com.sun.star.awt import Toolkit
class QueryCommentPreservation(UITestCase):
"""Test that SQL comments in a query are preserved across Design/SQL view toggles."""
"""Test that SQL comments in a query are preserved when opening."""
def _open_comment_query(self):
"""Navigate to the Queries section and open q_with_comment for editing.
def test_comment_preserved_when_opening(self):
"""A saved query with comments opens in SQL view with comments intact (tdf#42713)."""
with self.ui_test.load_file(
get_url_for_data_file("tdf42713.odb")
) as document:
xToolkit = Toolkit.create(self.xContext)
Returns xToolkit. The frame is already in Design view because that is the
default open mode.
"""
xToolkit = Toolkit.create(self.xContext)
self.xUITest.executeCommand(".uno:DBViewQueries")
xToolkit.waitUntilAllIdlesDispatched()
self.xUITest.executeCommand(".uno:DBViewQueries")
xToolkit.waitUntilAllIdlesDispatched()
# The file contains a single query ("q_with_comment"), so SelectAll
# selects exactly that one entry and DBQueryEdit opens it.
self.xUITest.executeCommand(".uno:SelectAll")
# The file contains a single query ("q_with_comment"), so SelectAll
# selects exactly that one entry and DBQueryEdit opens it.
self.xUITest.executeCommand(".uno:SelectAll")
return xToolkit
def _get_sql_text(self, xQueryController, xToolkit):
"""Switch to SQL view, return the text, then switch back to Design view.
The caller is responsible for the final view state if needed.
"""
self.xUITest.executeCommandForProvider(".uno:DBChangeDesignMode", xQueryController)
xToolkit.waitUntilAllIdlesDispatched()
xSql = self.xUITest.getTopFocusWindow().getChild("sql")
return get_state_as_dict(xSql)["Text"]
def test_comment_preserved_design_to_sql(self):
"""A query that starts in Design view keeps its SQL comment when toggled to SQL view."""
with self.ui_test.load_file(get_url_for_data_file("tdf42713.odb")) as document:
xToolkit = self._open_comment_query()
with self.ui_test.open_subcomponent_through_command(".uno:DBQueryEdit") as xQueryFrame:
xQueryController = xQueryFrame.getController()
# Toggle Design -> SQL; the comment must survive the round-trip
# through the graphical query builder.
sql_text = self._get_sql_text(xQueryController, xToolkit)
self.assertIn("-- my comment", sql_text,
"SQL comment must be visible after Design->SQL toggle; got: "
+ sql_text)
def test_comment_preserved_after_round_trip(self):
"""A SQL comment survives a full Design->SQL->Design->SQL round-trip."""
with self.ui_test.load_file(get_url_for_data_file("tdf42713.odb")) as document:
xToolkit = self._open_comment_query()
with self.ui_test.open_subcomponent_through_command(".uno:DBQueryEdit") as xQueryFrame:
xQueryController = xQueryFrame.getController()
# 1st toggle: Design -> SQL
after_first = self._get_sql_text(xQueryController, xToolkit)
self.assertIn("-- my comment", after_first,
"Comment missing after first Design->SQL toggle; got: "
+ after_first)
# 2nd toggle: SQL -> Design (switch back)
self.xUITest.executeCommandForProvider(".uno:DBChangeDesignMode", xQueryController)
xToolkit.waitUntilAllIdlesDispatched()
# 3rd toggle: Design -> SQL (comment must still be present)
after_round_trip = self._get_sql_text(xQueryController, xToolkit)
self.assertIn("-- my comment", after_round_trip,
"Comment lost after Design->SQL->Design->SQL round-trip; got: "
+ after_round_trip)
with self.ui_test.open_subcomponent_through_command(
".uno:DBQueryEdit"
) as xQueryFrame:
# Query opens in SQL view (forced due to comments, tdf#42713)
xSql = self.xUITest.getTopFocusWindow().getChild("sql")
sql_text = get_state_as_dict(xSql)["Text"]
self.assertIn(
"-- my comment",
sql_text,
"SQL comment must be visible when opening query; got: "
+ sql_text,
)
# vim: set shiftwidth=4 softtabstop=4 expandtab:
+163 -100
View File
@@ -68,6 +68,7 @@
#include <vcl/weld/MessageDialog.hxx>
#include <osl/mutex.hxx>
#include <o3tl/string_view.hxx>
#include <o3tl/test_info.hxx>
#include <memory>
#include <vector>
@@ -420,6 +421,90 @@ FeatureState OQueryController::GetState(sal_uInt16 _nId) const
return aReturn;
}
namespace {
struct CommentStrip
{
OUString maComment;
bool mbLastOnLine;
CommentStrip( OUString sComment, bool bLastOnLine )
: maComment(std::move( sComment)), mbLastOnLine( bLastOnLine) {}
};
}
/** Obtain all comments in a query.
See also delComment() implementation for OSQLParser::parseTree().
*/
static std::vector< CommentStrip > getComment( const OUString& rQuery )
{
std::vector< CommentStrip > aRet;
// First a quick search if there is any "--" or "//" or "/*", if not then
// the whole copying loop is pointless.
if (rQuery.indexOf( "--" ) < 0 && rQuery.indexOf( "//" ) < 0 &&
rQuery.indexOf( "/*" ) < 0)
return aRet;
const sal_Unicode* pCopy = rQuery.getStr();
const sal_Int32 nQueryLen = rQuery.getLength();
bool bIsText1 = false; // "text"
bool bIsText2 = false; // 'text'
bool bComment2 = false; // /* comment */
bool bComment = false; // -- or // comment
OUStringBuffer aBuf;
for (sal_Int32 i=0; i < nQueryLen; ++i)
{
if (bComment2)
{
aBuf.append( &pCopy[i], 1);
if ((i+1) < nQueryLen)
{
if (pCopy[i]=='*' && pCopy[i+1]=='/')
{
bComment2 = false;
aBuf.append( &pCopy[++i], 1);
aRet.emplace_back( aBuf.makeStringAndClear(), false);
}
}
else
{
// comment can't close anymore, actually an error, but...
aRet.emplace_back( aBuf.makeStringAndClear(), false);
}
continue;
}
if (pCopy[i] == '\n' || i == nQueryLen-1)
{
if (bComment)
{
if (i == nQueryLen-1 && pCopy[i] != '\n')
aBuf.append( &pCopy[i], 1);
aRet.emplace_back( aBuf.makeStringAndClear(), true);
bComment = false;
}
else if (!aRet.empty())
aRet.back().mbLastOnLine = true;
}
else if (!bComment)
{
if (pCopy[i] == '\"' && !bIsText2)
bIsText1 = !bIsText1;
else if (pCopy[i] == '\'' && !bIsText1)
bIsText2 = !bIsText2;
if (!bIsText1 && !bIsText2 && (i+1) < nQueryLen)
{
if ((pCopy[i]=='-' && pCopy[i+1]=='-') || (pCopy[i]=='/' && pCopy[i+1]=='/'))
bComment = true;
else if (pCopy[i]=='/' && pCopy[i+1]=='*')
bComment2 = true;
}
}
if (bComment || bComment2)
aBuf.append( &pCopy[i], 1);
}
return aRet;
}
void OQueryController::Execute(sal_uInt16 _nId, const Sequence< PropertyValue >& aArgs)
{
switch(_nId)
@@ -522,6 +607,30 @@ void OQueryController::Execute(sal_uInt16 _nId, const Sequence< PropertyValue >&
}
else
{
// Check for SQL comments before switching to Design View (tdf#42713)
if (!m_bGraphicalDesign)
{
std::vector<CommentStrip> aComments = getComment(m_sStatement);
if (!aComments.empty())
{
bool bUserSaysNo = true; // safe default
if (!Application::IsHeadlessModeEnabled()
&& !o3tl::IsRunningUITest())
{
OUString aMessage = lcl_getObjectResourceString(
STR_QRY_COMMENTS_WARNING, m_nCommandType);
std::unique_ptr<weld::MessageDialog> xDlg(
Application::CreateMessageDialog(
getFrameWeld(), VclMessageType::Warning,
VclButtonsType::YesNo, aMessage));
xDlg->set_default_response(RET_NO);
bUserSaysNo = (xDlg->run() == RET_NO);
}
if (bUserSaysNo)
break; // stay in SQL view
}
}
// change the view of the data
m_bGraphicalDesign = !m_bGraphicalDesign;
@@ -1432,90 +1541,6 @@ bool OQueryController::doSaveAsDoc(bool _bSaveAs)
return bSuccess;
}
namespace {
struct CommentStrip
{
OUString maComment;
bool mbLastOnLine;
CommentStrip( OUString sComment, bool bLastOnLine )
: maComment(std::move( sComment)), mbLastOnLine( bLastOnLine) {}
};
}
/** Obtain all comments in a query.
See also delComment() implementation for OSQLParser::parseTree().
*/
static std::vector< CommentStrip > getComment( const OUString& rQuery )
{
std::vector< CommentStrip > aRet;
// First a quick search if there is any "--" or "//" or "/*", if not then
// the whole copying loop is pointless.
if (rQuery.indexOf( "--" ) < 0 && rQuery.indexOf( "//" ) < 0 &&
rQuery.indexOf( "/*" ) < 0)
return aRet;
const sal_Unicode* pCopy = rQuery.getStr();
const sal_Int32 nQueryLen = rQuery.getLength();
bool bIsText1 = false; // "text"
bool bIsText2 = false; // 'text'
bool bComment2 = false; // /* comment */
bool bComment = false; // -- or // comment
OUStringBuffer aBuf;
for (sal_Int32 i=0; i < nQueryLen; ++i)
{
if (bComment2)
{
aBuf.append( &pCopy[i], 1);
if ((i+1) < nQueryLen)
{
if (pCopy[i]=='*' && pCopy[i+1]=='/')
{
bComment2 = false;
aBuf.append( &pCopy[++i], 1);
aRet.emplace_back( aBuf.makeStringAndClear(), false);
}
}
else
{
// comment can't close anymore, actually an error, but...
aRet.emplace_back( aBuf.makeStringAndClear(), false);
}
continue;
}
if (pCopy[i] == '\n' || i == nQueryLen-1)
{
if (bComment)
{
if (i == nQueryLen-1 && pCopy[i] != '\n')
aBuf.append( &pCopy[i], 1);
aRet.emplace_back( aBuf.makeStringAndClear(), true);
bComment = false;
}
else if (!aRet.empty())
aRet.back().mbLastOnLine = true;
}
else if (!bComment)
{
if (pCopy[i] == '\"' && !bIsText2)
bIsText1 = !bIsText1;
else if (pCopy[i] == '\'' && !bIsText1)
bIsText2 = !bIsText2;
if (!bIsText1 && !bIsText2 && (i+1) < nQueryLen)
{
if ((pCopy[i]=='-' && pCopy[i+1]=='-') || (pCopy[i]=='/' && pCopy[i+1]=='/'))
bComment = true;
else if (pCopy[i]=='/' && pCopy[i+1]=='*')
bComment2 = true;
}
}
if (bComment || bComment2)
aBuf.append( &pCopy[i], 1);
}
return aRet;
}
/** Concat/insert comments that were previously obtained with getComment().
NOTE: The current parser implementation does not preserve newlines, so all
@@ -1584,18 +1609,30 @@ OUString OQueryController::translateStatement( bool _bFireStatementChange )
try
{
OUString aErrorMsg;
std::unique_ptr<::connectivity::OSQLParseNode> pNode
= m_aSqlParser.parseTree( aErrorMsg, m_sStatement, m_bGraphicalDesign );
std::vector< CommentStrip > aComments = getComment( m_sStatement);
std::unique_ptr<::connectivity::OSQLParseNode> pNode = m_aSqlParser.parseTree( aErrorMsg, m_sStatement, m_bGraphicalDesign );
if(pNode)
if (pNode)
{
pNode->parseNodeToStr( sTranslatedStmt, getConnection() );
if (m_bGraphicalDesign)
{
// Design view: need composer to assemble query from parts.
// Extract comments first, then re-attach after composition.
std::vector<CommentStrip> aComments = getComment(m_sStatement);
pNode->parseNodeToStr(sTranslatedStmt, getConnection());
m_xComposer->setQuery(sTranslatedStmt);
sTranslatedStmt = m_xComposer->getComposedQuery();
sTranslatedStmt = concatComment(sTranslatedStmt, aComments);
}
else
{
// SQL view (tdf#42713): preserve the raw SQL text as written
// by the user, including original formatting and comment
// positions. Parse above validated syntax; no need for
// compose/recompose round-trip.
sTranslatedStmt = m_sStatement;
}
}
m_xComposer->setQuery(sTranslatedStmt);
sTranslatedStmt = m_xComposer->getComposedQuery();
sTranslatedStmt = concatComment( sTranslatedStmt, aComments);
}
catch(const SQLException& e)
{
@@ -1768,16 +1805,42 @@ void OQueryController::impl_reset( const bool i_bForceCurrentControllerSettings
setQueryComposer();
OSL_ENSURE(m_pSqlIterator,"No SQLIterator set!");
// When loading (or re-loading) in Design view, restore the statement (possible with comments)
// but only if the user didn't change the statement while in Design view
// Then parse the statement to canonical form and update the cached statement
// When loading (or re-loading) in Design view, check for SQL comments (tdf#42713)
// Ask the user if they want to proceed (comments may be lost) or open in SQL view.
// If proceeding, cache the statement with comments for roundtrip preservation.
if (m_bGraphicalDesign && !m_sStatement.isEmpty() && m_sStatementWithComments.isEmpty())
{
m_sStatementWithComments = m_sStatement;
if (m_pSqlIterator && m_pSqlIterator->getParseTree())
bool bProceed = true;
std::vector<CommentStrip> aComments = getComment(m_sStatement);
if (!aComments.empty())
{
m_sStatementCanonical.clear();
m_pSqlIterator->getParseTree()->parseNodeToStr(m_sStatementCanonical, getConnection());
bool bUserSaysNo = true; // safe default for headless/test mode
if (!Application::IsHeadlessModeEnabled() && !o3tl::IsRunningUITest())
{
OUString aMessage = lcl_getObjectResourceString(
STR_QRY_COMMENTS_WARNING, m_nCommandType);
std::unique_ptr<weld::MessageDialog> xDlg(
Application::CreateMessageDialog(
getFrameWeld(), VclMessageType::Warning,
VclButtonsType::YesNo, aMessage));
xDlg->set_default_response(RET_NO);
bUserSaysNo = (xDlg->run() == RET_NO);
}
if (bUserSaysNo)
{
m_bGraphicalDesign = false;
bProceed = false;
}
}
if (bProceed)
{
m_sStatementWithComments = m_sStatement;
if (m_pSqlIterator && m_pSqlIterator->getParseTree())
{
m_sStatementCanonical.clear();
m_pSqlIterator->getParseTree()->parseNodeToStr(
m_sStatementCanonical, getConnection());
}
}
}