diff --git a/docs/en_US/images/preferences_sql_formatting.png b/docs/en_US/images/preferences_sql_formatting.png index beebfa131..02d6659f8 100644 Binary files a/docs/en_US/images/preferences_sql_formatting.png and b/docs/en_US/images/preferences_sql_formatting.png differ diff --git a/docs/en_US/preferences.rst b/docs/en_US/preferences.rst index ccba54014..f754340f4 100644 --- a/docs/en_US/preferences.rst +++ b/docs/en_US/preferences.rst @@ -486,26 +486,27 @@ Query Tool window navigation: Use the fields on the *SQL formatting* panel to specify your preferences for reformatting of SQL. -* Use the *Comma-first notation* option to specify whether to place commas - before or after column names. +* Use the *Data type case* option to specify whether to change data types + into upper, lower, or preserve case. +* Use the *Expression width* option to specify maximum number of characters + in parenthesized expressions to be kept on single line. +* Use the *Function case* option to specify whether to change function + names into upper, lower, or preserve case. * Use the *Identifier case* option to specify whether to change identifiers (object names) into upper, lower, or capitalized case. * Use the *Keyword case* option to specify whether to change keywords into - upper, lower, or capitalized case. -* Use the *Re-indent aligned?* option to specify that indentations of statements - should be changed, aligned by keywords. -* Use the *Re-indent?* option to specify that indentations of statements should - be changed. + upper, lower, or preserve case. +* Use *Lines between queries* to specify how many empty lines to leave + between SQL statements. If set to zero it puts no new line. +* Use *Logical operator new line* to specify newline placement before or + after logical operators (AND, OR, XOR). +* Use *New line before semicolon?* to specify whether to place query + separator (;) on a separate line. * Use the *Spaces around operators?* option to specify whether or not to include spaces on either side of operators. -* Use the *Strip comments?* option to specify whether or not comments should be - removed. * Use the *Tab size* option to specify the number of spaces per tab or indent. * Use the *Use spaces?* option to select whether to use spaces or tabs when indenting. -* Use the *Wrap after N characters* option to specify the column limit for - wrapping column separated lists (e.g. of column names in a table). If set to - 0 (zero), each item will be on it's own line. The Schema Diff Node ******************** diff --git a/web/package.json b/web/package.json index 7927ed5c4..4f16f385d 100644 --- a/web/package.json +++ b/web/package.json @@ -152,6 +152,7 @@ "snapsvg-cjs": "^0.0.6", "socket.io-client": "^4.5.0", "split.js": "^1.5.10", + "sql-formatter": "^15.1.2", "styled-components": "^5.2.1", "uplot": "^1.6.24", "uplot-react": "^1.1.4", diff --git a/web/pgadmin/misc/__init__.py b/web/pgadmin/misc/__init__.py index 4cd0141de..6b3138393 100644 --- a/web/pgadmin/misc/__init__.py +++ b/web/pgadmin/misc/__init__.py @@ -120,9 +120,6 @@ class MiscModule(PgAdminModule): from .file_manager import blueprint as module self.submodules.append(module) - from .sql import blueprint as module - self.submodules.append(module) - from .statistics import blueprint as module self.submodules.append(module) diff --git a/web/pgadmin/misc/sql/__init__.py b/web/pgadmin/misc/sql/__init__.py deleted file mode 100644 index 55d25de13..000000000 --- a/web/pgadmin/misc/sql/__init__.py +++ /dev/null @@ -1,80 +0,0 @@ -########################################################################## -# -# pgAdmin 4 - PostgreSQL Tools -# -# Copyright (C) 2013 - 2024, The pgAdmin Development Team -# This software is released under the PostgreSQL Licence -# -########################################################################## - -"""A blueprint module providing utility functions for the application.""" - -import sqlparse -from flask import request, url_for -from flask_security import login_required -from pgadmin.utils import PgAdminModule -from pgadmin.utils.ajax import make_json_response -from pgadmin.utils.preferences import Preferences - -MODULE_NAME = 'sql' - - -class SQLModule(PgAdminModule): - def get_exposed_url_endpoints(self): - """ - Returns: - list: URL endpoints - """ - return [ - 'sql.format', 'sql.format' - ] - - -# Initialise the module -blueprint = SQLModule(MODULE_NAME, __name__, url_prefix='/misc/sql') - - -def sql_format(sql): - """ - This function takes a SQL statement, formats it, and returns it - """ - p = Preferences.module('sqleditor') - use_spaces = p.preference('use_spaces').get() - output = sqlparse.format(sql, - keyword_case=p.preference( - 'keyword_case').get(), - identifier_case=p.preference( - 'identifier_case').get(), - strip_comments=p.preference( - 'strip_comments').get(), - reindent=p.preference( - 'reindent').get(), - reindent_aligned=p.preference( - 'reindent_aligned').get(), - use_space_around_operators=p.preference( - 'spaces_around_operators').get(), - comma_first=p.preference( - 'comma_first').get(), - wrap_after=p.preference( - 'wrap_after').get(), - indent_tabs=not use_spaces, - indent_width=p.preference( - 'tab_size').get() if use_spaces else 1) - - return output - - -@blueprint.route("/format", methods=['POST'], endpoint="format") -@login_required -def sql_format_wrapper(): - """ - This endpoint takes a SQL statement, formats it, and returns it - """ - sql = '' - if request.data: - sql = sql_format(request.get_json()['sql']) - - return make_json_response( - data={'sql': sql}, - status=200 - ) diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx index bf93204d0..ac68c2634 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx @@ -8,6 +8,7 @@ ////////////////////////////////////////////////////////////// import { makeStyles } from '@material-ui/styles'; import React, {useContext, useCallback, useEffect } from 'react'; +import { format } from 'sql-formatter'; import { QueryToolContext, QueryToolEventsContext } from '../QueryToolComponent'; import CodeMirror from '../../../../../../static/js/components/CodeMirror'; import {PANELS, QUERY_TOOL_EVENTS} from '../QueryToolConstants'; @@ -444,19 +445,35 @@ export default function Query() { }); eventBus.registerListener(QUERY_TOOL_EVENTS.TRIGGER_FORMAT_SQL, ()=>{ let selection = true, sql = editor.current?.getSelection(); + let sqlEditorPref = preferencesStore.getPreferencesForModule('sqleditor'); + /* New library does not support capitalize casing + so if a user has set capitalize casing we will + use preserve casing which is default for the library. + */ + let formatPrefs = { + language: 'postgresql', + keywordCase: sqlEditorPref.keyword_case === 'capitalize' ? 'preserve' : sqlEditorPref.keyword_case, + identifierCase: sqlEditorPref.identifier_case === 'capitalize' ? 'preserve' : sqlEditorPref.identifier_case, + dataTypeCase: sqlEditorPref.data_type_case, + functionCase: sqlEditorPref.function_case, + logicalOperatorNewline: sqlEditorPref.logical_operator_new_line, + expressionWidth: sqlEditorPref.expression_width, + linesBetweenQueries: sqlEditorPref.lines_between_queries, + tabWidth: sqlEditorPref.tab_size, + useTabs: !sqlEditorPref.use_spaces, + denseOperators: !sqlEditorPref.spaces_around_operators, + newlineBeforeSemicolon: sqlEditorPref.new_line_before_semicolon + }; if(sql == '') { sql = editor.current.getValue(); selection = false; } - queryToolCtx.api.post(url_for('sql.format'), { - 'sql': sql, - }).then((res)=>{ - if(selection) { - editor.current.replaceSelection(res.data.data.sql, 'around'); - } else { - editor.current.setValue(res.data.data.sql); - } - }).catch(()=>{/* failure should be ignored */}); + let formattedSql = format(sql,formatPrefs); + if(selection) { + editor.current.replaceSelection(formattedSql, 'around'); + } else { + editor.current.setValue(formattedSql); + } }); eventBus.registerListener(QUERY_TOOL_EVENTS.EDITOR_TOGGLE_CASE, ()=>{ let selectedText = editor.current?.getSelection(); diff --git a/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py b/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py index 8485b75ee..1255e979a 100644 --- a/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py +++ b/web/pgadmin/tools/sqleditor/utils/query_tool_preferences.py @@ -735,10 +735,10 @@ def register_query_tool_preferences(self): gettext("Keyword case"), 'radioModern', 'upper', options=[{'label': gettext('Upper case'), 'value': 'upper'}, {'label': gettext('Lower case'), 'value': 'lower'}, - {'label': gettext('Capitalized'), 'value': 'capitalize'}], + {'label': gettext('Preserve'), 'value': 'preserve'}], category_label=PREF_LABEL_SQL_FORMATTING, help_str=gettext( - 'Convert keywords to upper, lower, or capitalized casing.' + 'Convert keywords to upper, lower, or preserve casing.' ) ) @@ -747,35 +747,35 @@ def register_query_tool_preferences(self): gettext("Identifier case"), 'radioModern', 'upper', options=[{'label': gettext('Upper case'), 'value': 'upper'}, {'label': gettext('Lower case'), 'value': 'lower'}, - {'label': gettext('Capitalized'), 'value': 'capitalize'}], + {'label': gettext('Preserve'), 'value': 'preserve'}], category_label=PREF_LABEL_SQL_FORMATTING, help_str=gettext( - 'Convert identifiers to upper, lower, or capitalized casing.' + 'Convert identifiers to upper, lower, or preserve casing.' ) ) - self.strip_comments = self.preference.register( - 'editor', 'strip_comments', - gettext("Strip comments?"), 'boolean', False, + self.function_case = self.preference.register( + 'editor', 'function_case', + gettext("Function case"), 'radioModern', 'upper', + options=[{'label': gettext('Upper case'), 'value': 'upper'}, + {'label': gettext('Lower case'), 'value': 'lower'}, + {'label': gettext('Preserve'), 'value': 'preserve'}], category_label=PREF_LABEL_SQL_FORMATTING, - help_str=gettext('If set to True, comments will be removed.') + help_str=gettext( + 'Convert function names to upper, lower, or preserve casing.' + ) ) - self.reindent = self.preference.register( - 'editor', 'reindent', - gettext("Re-indent?"), 'boolean', True, + self.data_type_case = self.preference.register( + 'editor', 'data_type_case', + gettext("Data type case"), 'radioModern', 'upper', + options=[{'label': gettext('Upper case'), 'value': 'upper'}, + {'label': gettext('Lower case'), 'value': 'lower'}, + {'label': gettext('Preserve'), 'value': 'preserve'}], category_label=PREF_LABEL_SQL_FORMATTING, - help_str=gettext('If set to True, the indentations of the ' - 'statements are changed.') - ) - - self.reindent_aligned = self.preference.register( - 'editor', 'reindent_aligned', - gettext("Re-indent aligned?"), 'boolean', False, - category_label=PREF_LABEL_SQL_FORMATTING, - help_str=gettext('If set to True, the indentations of the ' - 'statements are changed, and statements are ' - 'aligned by keywords.') + help_str=gettext( + 'Convert data types to upper, lower, or preserve casing.' + ) ) self.spaces_around_operators = self.preference.register( @@ -786,23 +786,6 @@ def register_query_tool_preferences(self): 'operators.') ) - self.comma_first = self.preference.register( - 'editor', 'comma_first', - gettext("Comma-first notation?"), 'boolean', False, - category_label=PREF_LABEL_SQL_FORMATTING, - help_str=gettext('If set to True, comma-first notation for column ' - 'names is used.') - ) - - self.wrap_after = self.preference.register( - 'editor', 'wrap_after', - gettext("Wrap after N characters"), 'integer', 4, - category_label=PREF_LABEL_SQL_FORMATTING, - help_str=gettext("The column limit (in characters) for wrapping " - "comma-separated lists. If zero, it puts " - "every item in the list on its own line.") - ) - self.tab_size = self.preference.register( 'editor', 'tab_size', gettext("Tab size"), 'integer', 4, @@ -824,6 +807,49 @@ def register_query_tool_preferences(self): ) ) + self.expression_width = self.preference.register( + 'editor', 'expression_width', + gettext("Expression Width"), 'integer', 50, + category_label=PREF_LABEL_SQL_FORMATTING, + help_str=gettext( + 'maximum number of characters in parenthesized expressions to be ' + 'kept on single line.' + ) + ) + + self.logical_operator_new_line = self.preference.register( + 'editor', 'logical_operator_new_line', + gettext("Logical operator new line"), 'radioModern', 'before', + options=[{'label': gettext('Before'), 'value': 'before'}, + {'label': gettext('After'), 'value': 'after'}], + category_label=PREF_LABEL_SQL_FORMATTING, + help_str=gettext( + 'Decides newline placement before or after logical operators ' + '(AND, OR, XOR).' + ) + ) + + self.lines_between_queries = self.preference.register( + 'editor', 'lines_between_queries', + gettext("Lines between queries"), 'integer', 1, + min_val=0, + max_val=5, + category_label=PREF_LABEL_SQL_FORMATTING, + help_str=gettext( + 'Decides how many empty lines to leave between SQL statements. ' + 'If zero it puts no new line.' + ) + ) + + self.new_line_before_semicolon = self.preference.register( + 'editor', 'new_line_before_semicolon', + gettext("New line before semicolon?"), 'boolean', False, + category_label=PREF_LABEL_SQL_FORMATTING, + help_str=gettext( + 'Whether to place query separator (;) on a separate line.' + ) + ) + self.row_limit = self.preference.register( 'graph_visualiser', 'row_limit', gettext("Row Limit"), 'integer', diff --git a/web/yarn.lock b/web/yarn.lock index 6b462d6ee..e32d5c91a 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -6237,7 +6237,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^2.20.0, commander@npm:^2.8.1": +"commander@npm:^2.19.0, commander@npm:^2.20.0, commander@npm:^2.8.1": version: 2.20.3 resolution: "commander@npm:2.20.3" checksum: ab8c07884e42c3a8dbc5dd9592c606176c7eb5c1ca5ff274bcf907039b2c41de3626f684ea75ccf4d361ba004bbaff1f577d5384c155f3871e456bdf27becf9e @@ -7260,6 +7260,13 @@ __metadata: languageName: node linkType: hard +"discontinuous-range@npm:1.0.0": + version: 1.0.0 + resolution: "discontinuous-range@npm:1.0.0" + checksum: 8ee88d7082445b6eadc7c03bebe6dc978f96760c45e9f65d16ca66174d9e086a9e3855ee16acf65625e1a07a846a17de674f02a5964a6aebe5963662baf8b5c8 + languageName: node + linkType: hard + "dnd-core@npm:14.0.1": version: 14.0.1 resolution: "dnd-core@npm:14.0.1" @@ -8714,6 +8721,13 @@ __metadata: languageName: node linkType: hard +"get-stdin@npm:=8.0.0": + version: 8.0.0 + resolution: "get-stdin@npm:8.0.0" + checksum: 40128b6cd25781ddbd233344f1a1e4006d4284906191ed0a7d55ec2c1a3e44d650f280b2c9eeab79c03ac3037da80257476c0e4e5af38ddfb902d6ff06282d77 + languageName: node + linkType: hard + "get-stream@npm:3.0.0, get-stream@npm:^3.0.0": version: 3.0.0 resolution: "get-stream@npm:3.0.0" @@ -11877,6 +11891,13 @@ __metadata: languageName: node linkType: hard +"moo@npm:^0.5.0": + version: 0.5.2 + resolution: "moo@npm:0.5.2" + checksum: 5a41ddf1059fd0feb674d917c4774e41c877f1ca980253be4d3aae1a37f4bc513f88815041243f36f5cf67a62fb39324f3f997cf7fb17b6cb00767c165e7c499 + languageName: node + linkType: hard + "mousetrap@npm:^1.6.3": version: 1.6.5 resolution: "mousetrap@npm:1.6.5" @@ -11956,6 +11977,23 @@ __metadata: languageName: node linkType: hard +"nearley@npm:^2.20.1": + version: 2.20.1 + resolution: "nearley@npm:2.20.1" + dependencies: + commander: ^2.19.0 + moo: ^0.5.0 + railroad-diagrams: ^1.0.0 + randexp: 0.4.6 + bin: + nearley-railroad: bin/nearley-railroad.js + nearley-test: bin/nearley-test.js + nearley-unparse: bin/nearley-unparse.js + nearleyc: bin/nearleyc.js + checksum: 42c2c330c13c7991b48221c5df00f4352c2f8851636ae4d1f8ca3c8e193fc1b7668c78011d1cad88cca4c1c4dc087425420629c19cc286d7598ec15533aaef26 + languageName: node + linkType: hard + "neatequal@npm:^1.0.0": version: 1.0.0 resolution: "neatequal@npm:1.0.0" @@ -13709,6 +13747,23 @@ __metadata: languageName: node linkType: hard +"railroad-diagrams@npm:^1.0.0": + version: 1.0.0 + resolution: "railroad-diagrams@npm:1.0.0" + checksum: 9e312af352b5ed89c2118edc0c06cef2cc039681817f65266719606e4e91ff6ae5374c707cc9033fe29a82c2703edf3c63471664f97f0167c85daf6f93496319 + languageName: node + linkType: hard + +"randexp@npm:0.4.6": + version: 0.4.6 + resolution: "randexp@npm:0.4.6" + dependencies: + discontinuous-range: 1.0.0 + ret: ~0.1.10 + checksum: 3c0d440a3f89d6d36844aa4dd57b5cdb0cab938a41956a16da743d3a3578ab32538fc41c16cc0984b6938f2ae4cbc0216967e9829e52191f70e32690d8e3445d + languageName: node + linkType: hard + "randombytes@npm:^2.0.0, randombytes@npm:^2.0.1, randombytes@npm:^2.0.5, randombytes@npm:^2.1.0": version: 2.1.0 resolution: "randombytes@npm:2.1.0" @@ -14676,6 +14731,13 @@ __metadata: languageName: node linkType: hard +"ret@npm:~0.1.10": + version: 0.1.15 + resolution: "ret@npm:0.1.15" + checksum: d76a9159eb8c946586567bd934358dfc08a36367b3257f7a3d7255fdd7b56597235af23c6afa0d7f0254159e8051f93c918809962ebd6df24ca2a83dbe4d4151 + languageName: node + linkType: hard + "retry@npm:^0.12.0": version: 0.12.0 resolution: "retry@npm:0.12.0" @@ -14868,6 +14930,7 @@ __metadata: snapsvg-cjs: ^0.0.6 socket.io-client: ^4.5.0 split.js: ^1.5.10 + sql-formatter: ^15.1.2 style-loader: ^3.3.2 styled-components: ^5.2.1 stylis: ^4.0.7 @@ -15508,6 +15571,19 @@ __metadata: languageName: node linkType: hard +"sql-formatter@npm:^15.1.2": + version: 15.1.2 + resolution: "sql-formatter@npm:15.1.2" + dependencies: + argparse: ^2.0.1 + get-stdin: =8.0.0 + nearley: ^2.20.1 + bin: + sql-formatter: bin/sql-formatter-cli.cjs + checksum: 77379dd209bd65bac89f53d28e8e409c32878f008ec37572dcee59231db306c690771cf3a318e02a84a920d8059f22bb1e30d197ea29c1abecbfcd96c3e672ff + languageName: node + linkType: hard + "ssri@npm:^10.0.0": version: 10.0.4 resolution: "ssri@npm:10.0.4"