diff --git a/localization/l10n/bundle.l10n.json b/localization/l10n/bundle.l10n.json index d0dc65a4af..bff73f737a 100644 --- a/localization/l10n/bundle.l10n.json +++ b/localization/l10n/bundle.l10n.json @@ -744,6 +744,31 @@ "Show Confirm Password": "Show Confirm Password", "Hide Confirm Password": "Hide Confirm Password", "Passwords do not match": "Passwords do not match", + "Save Changes": "Save Changes", + "Add Row": "Add Row", + "Show Script": "Show Script", + "Hide Script": "Hide Script", + "Open in SQL Editor": "Open in SQL Editor", + "Copy Script": "Copy Script", + "Copy Script to Clipboard": "Copy Script to Clipboard", + "Maximize Panel Size": "Maximize Panel Size", + "Restore Panel Size": "Restore Panel Size", + "Update Script": "Update Script", + "Commands": "Commands", + "Delete Row": "Delete Row", + "Revert Cell": "Revert Cell", + "Revert Row": "Revert Row", + "Total rows to fetch:": "Total rows to fetch:", + "Rows per page": "Rows per page", + "Fetch rows": "Fetch rows", + "First Page": "First Page", + "Previous Page": "Previous Page", + "Next Page": "Next Page", + "Last Page": "Last Page", + "Loading table data...": "Loading table data...", + "No data available": "No data available", + "No pending changes. Make edits to generate a script.": "No pending changes. Make edits to generate a script.", + "Close Script Pane": "Close Script Pane", "Object Explorer Filter": "Object Explorer Filter", "Azure MFA": "Azure MFA", "Windows Authentication": "Windows Authentication", @@ -1833,6 +1858,61 @@ "message": "Edit Connection Group - {0}", "comment": ["{0} is the connection group name"] }, + "Unable to open Table Explorer: No target node provided.": "Unable to open Table Explorer: No target node provided.", + "Changes saved successfully.": "Changes saved successfully.", + "Row created.": "Row created.", + "Row removed.": "Row removed.", + "Table Explorer: {0} (Preview)/{0} is the table name": { + "message": "Table Explorer: {0} (Preview)", + "comment": ["{0} is the table name"] + }, + "Failed to save changes: {0}/{0} is the error message": { + "message": "Failed to save changes: {0}", + "comment": ["{0} is the error message"] + }, + "Failed to load data: {0}/{0} is the error message": { + "message": "Failed to load data: {0}", + "comment": ["{0} is the error message"] + }, + "Failed to create a new row: {0}/{0} is the error message": { + "message": "Failed to create a new row: {0}", + "comment": ["{0} is the error message"] + }, + "Failed to remove row: {0}/{0} is the error message": { + "message": "Failed to remove row: {0}", + "comment": ["{0} is the error message"] + }, + "Failed to update cell: {0}/{0} is the error message": { + "message": "Failed to update cell: {0}", + "comment": ["{0} is the error message"] + }, + "Failed to revert cell: {0}/{0} is the error message": { + "message": "Failed to revert cell: {0}", + "comment": ["{0} is the error message"] + }, + "Failed to revert row: {0}/{0} is the error message": { + "message": "Failed to revert row: {0}", + "comment": ["{0} is the error message"] + }, + "Failed to generate script: {0}/{0} is the error message": { + "message": "Failed to generate script: {0}", + "comment": ["{0} is the error message"] + }, + "No script available. Make changes to the table data and generate a script first.": "No script available. Make changes to the table data and generate a script first.", + "Failed to open script: {0}/{0} is the error message": { + "message": "Failed to open script: {0}", + "comment": ["{0} is the error message"] + }, + "Script copied to clipboard.": "Script copied to clipboard.", + "Failed to copy script: {0}/{0} is the error message": { + "message": "Failed to copy script: {0}", + "comment": ["{0} is the error message"] + }, + "Table Explorer for '{0}' has unsaved changes. Do you want to save or discard them?/{0} is the table name": { + "message": "Table Explorer for '{0}' has unsaved changes. Do you want to save or discard them?", + "comment": ["{0} is the table name"] + }, + "Discard": "Discard", "Azure sign in failed.": "Azure sign in failed.", "Select subscriptions": "Select subscriptions", "Error loading Azure subscriptions.": "Error loading Azure subscriptions.", diff --git a/localization/xliff/vscode-mssql.xlf b/localization/xliff/vscode-mssql.xlf index 503f84af13..93fe949fe2 100644 --- a/localization/xliff/vscode-mssql.xlf +++ b/localization/xliff/vscode-mssql.xlf @@ -1,4537 +1,4684 @@ - - - - - is required. - - - $(plug) Connect to MSSQL - - - <Default> - - - <default> - - - (0 rows affected) - - - (1 row affected) - - - ({0} rows affected) - {0} is the number of rows affected - - - + Add Azure Account - - - + Add Fabric Account - - - + Create Connection Group - - - A SQL editor must have focus before executing this command - - - A firewall rule is required to access this server. - - - A highly integrated, developer-ready transactional database that auto-scales, auto-tunes, and mirrors data to OneLake for analytics across Fabric services - - - A predefined global default value for the column or binding. - - - A query is already running for this editor session. Please cancel this query or wait for its completion. - - - Accelerate schema evolution by autogenerating ORM migrations or T-SQL change scripts - - - Accept - - - Accept the SQL Server EULA to deploy a SQL Server Docker container - - - Access denied. Please ensure you have the necessary permissions to use this tool or model. - - - Access token expired for connection {0} with uri {1} - {0} is the connection id -{1} is the uri - - - Account - - - Account not found - - - Action - - - Actual Elapsed CPU Time - - - Actual Elapsed Time - - - Actual Number of Rows For All Executions - - - Add - - - Add Account - - - Add Column - - - Add Connection - - - Add Firewall Rule - - - Add Firewall Rule to {0} - {0} is the server name - - - Add Server Connection - - - Add Table - - - Add a Microsoft Entra account... - - - Add account - - - Add my client IP ({0}) - {0} is the IP address of the client - - - Add new column - - - Add new foreign key - - - Additional parameters - - - Advanced - - - Advanced Connection Settings - - - Advanced Options - - - All permissions for extensions to access your connections have been cleared. - - - Allow Null - - - Allow Nulls - - - Allow this extension to access your connections - - - Alphabetical - - - Alter - - - Always Encrypted - - - Always show in new tab - - - An active connection is required for GitHub Copilot to understand your database schema and proceed. Select "{0}" to establish a connection. - {0} is the button text (e.g., 'Connect' or 'Open SQL editor and connect') - - - An error occurred refreshing nodes. See the MSSQL output channel for more details. - - - An error occurred while copying results: {0} - {0} is the error message - - - An error occurred while processing your request. - - - An error occurred while removing Microsoft Entra account: {0} - {0} is the error message - - - An error occurred while retrieving rows: {0} - {0} is the error message - - - An error occurred while searching database objects - - - An error occurred while searching database objects: {0} - {0} is the error detail returned from the search operation - - - An error occurred: {0} - {0} is the error message - - - An unexpected error occurred with the language model. Please try again. - - - An unknown error occurred. Please try again. - - - Analytics-ready by default - - - And - - - Application Intent - - - Apply - - - Apply changes to target - - - Apply contextual suggestions for SQL syntax, relationships, and constraints - - - Approve - - - Are you sure you want to delete the container {0}? This will remove both the container and its connection from VS Code. - {0} is the container name - - - Are you sure you want to delete the selected items? - - - Are you sure you want to delete {0}? - {0} is the group name - - - Are you sure you want to delete {0}? You can delete its connections as well, or move them to the root folder. - {0} is the group name - - - Are you sure you want to disconnect? - - - Are you sure you want to remove {0}? - {0} is the node label - - - Are you sure you want to update the target? - - - Are you sure you want to {0}? - {0} is the action being confirmed - - - Are you sure? - - - Authentication - - - Authentication Library has changed, please reload Visual Studio Code. - - - Authentication Type - - - Authentication error for account '{0}' (tenant '{1}'). Resolving this requires clearing your token cache, which will sign you out of all connected accounts. - {0} is the account display name -{1} is the tenant id - - - Authentication error for account. Resolving this requires clearing your token cache, which will sign you out of all connected accounts. - - - Authentication failed due to a nonce mismatch, please close Azure Data Studio and try again. - - - Authentication failed due to a state mismatch, please close ADS and try again. - - - Auto Arrange - - - Auto Arrange Confirmation - - - Auto Arrange will automatically reposition all diagram elements based on optimal layout algorithms. Any custom positioning you've created will be lost. Do you want to proceed with auto-arranging your schema diagram? - - - Automatic tuning features like automatic index creation enabled by default. - - - Available Servers - - - Average: {0} - {0} is the average - - - Average: {0} Count: {1} Sum: {2} - {0} is the average, {1} is the count, {2} is the sum - - - Azure (China) - - - Azure (Public) - - - Azure (US Government) - - - Azure Account - - - Azure Code Grant - - - Azure Device Code - - - Azure MFA - - - Azure sign in failed. - - - Azure: Sign In - - - Azure: Sign In to Azure Cloud - - - Azure: Sign In with Device Code - - - Back - - - Back to preview - - - Batch execution time: {0} - {0} is the batch time - - - Between - - - Blanks - - - Block this extension from accessing your connections - - - Brightness - - - Browse Azure - - - Browse By - - - Browse Fabric - - - CSV - - - Calling tool: {0} with {1}. - {0} is the tool function name -{1} is the SQL tool parameters - - - Cancel - - - Cancel failed: {0} - {0} is the error message - - - Cancel schema compare failed: '{0}' - {0} is the error message returned from the cancel operation - - - Canceled - - - Canceling - - - Canceling query - - - Canceling the query failed: {0} - {0} is the error message - - - Cannot cancel query as no query is running. - - - Cannot connect due to expired tokens. Please re-authenticate and try again. - - - Cannot create foreign key - - - Cannot exclude {0}. Included dependents exist - {0} is the name of the entry - - - Cannot exclude {0}. Included dependents exist, such as {1} - {0} is the name of the entry -{1} is the name of the blocking dependency preventing exclusion. - - - Cannot include {0}. Excluded dependents exist - {0} is the name of the entry - - - Cannot include {0}. Excluded dependents exist, such as {1} - {0} is the name of the entry -{1} is the name of the blocking dependency preventing inclusion. - - - Cascade - - - Change - - - Change Database - - - Change Password - - - Change database to '{1}' for connection '{0}'? - {0} is the connection ID -{1} is the database name - - - Changed Tables - - - Changed database context to "{0}" for document "{1}" - {0} is the database name -{1} is the document name - - - Changed database context to "{0}" on server "{1}" on document "{2}". - {0} is the database name -{1} is the server name -{2} is the document name - - - Changes published successfully - - - Changing database context to "{0}" on server "{1}" on document "{2}". - {0} is the database name -{1} is the server name -{2} is the document name - - - Changing database to '{1}' for connection '{0}' - {0} is the connection ID -{1} is the database name - - - Chat command not available in this VS Code version - - - Check Constraint - - - Check Constraints - - - Checking Docker Engine Configuration - - - Checking if Docker is installed - - - Checking if Docker is installed on your machine - - - Checking if Docker is running on your machine. If not, we'll start it for you. - - - Checking if Docker is started - - - Checking if the Docker Engine is configured correctly on your machine. - - - Checking pre-requisites - - - Choose An Action - - - Choose Query History - - - Choose SQL Language - - - Choose a Microsoft Entra account - - - Choose a Microsoft Entra tenant - - - Choose a connection profile from the list below - - - Choose a database from the list below - - - Choose a hostname for the container - - - Choose a name for the SQL Server Docker Container - - - Choose a port to host the SQL Server Docker Container - - - Choose an option to provision a database - - - Choose color - - - Choose the Right Version - - - Circular reference detected: '{0}' → '{1}' creates a cycle - {0} is source table -{1} is target table - - - Clear - - - Clear All - - - Clear Recent Connections List - - - Clear Selection - - - Clear Sort - - - Clear cache and refresh token - - - Clear permissions for all extensions to access your connections - - - Clear token cache - - - Click to cancel loading summary - - - Click to connect to a database - - - Click to load summary - - - Click to sign into an Azure account - - - Close - - - Close Designer - - - Close Find - - - Close properties pane - - - Close the current connection - - - Cloud - - - Collapse - - - Collapse All - - - Collapse Workspace Explorer - - - Color - - - Column - - - Column '{0}' already exists - {0} is the column name - - - Column '{0}' already has a foreign key - {0} is the column name - - - Column '{0}' cannot be null because it is a primary key - {0} is the column name - - - Column '{0}' is an identity column and cannot have a cascading foreign key - {0} is the column name - - - Column '{0}' must be a primary key - {0} is the referenced column - - - Column '{0}' not found - {0} is the column name - - - Column Name - - - Column max length cannot be empty - - - Column name cannot be empty - - - Column width must be at least {0} pixels. - {0} is the minimum column width in pixels - - - Columns - - - Columns in the primary key. - - - Command Timeout - - - Compare - - - Compare SQL Server editions - - - Comparison Details - - - Configure Linux containers - - - Configure Rosetta in Docker Desktop - - - Configure and customize SQL Server containers - - - Confirm Password - - - Confirm SQL Server admin password - - - Confirm new password - - - Confirm to clear recent connections list - - - Confirm to remove this profile. - - - Connect - - - Connect to Database - - - Connect to MSSQL - - - Connect to Server - - - Connect to a database - - - Connect to server {0} and database {1}? - {0} is the server name -{1} is the database name - - - Connect to server {0}? - {0} is the server name - - - Connect to {0} - {0} is the name of the connection profile - - - Connect using profile {0}? - {0} is the profile ID - - - Connected successfully - - - Connected to server "{0}" on document "{1}". Server information: {2} - {0} is the server name -{1} is the document name -{2} is the server info - - - Connected to: - - - Connecting - - - Connecting to Container - - - Connecting to Database - - - Connecting to server "{0}" on document "{1}". - {0} is the server name -{1} is the document name - - - Connecting to server {0} - {0} is the server name - - - Connecting to server {0} and database {1} - {0} is the server name -{1} is the database name - - - Connecting to your SQL Server Docker container - - - Connecting to {0}... - {0} is the connection display name - - - Connecting to: - - - Connecting using profile {0} - {0} is the profile ID - - - Connecting... - - - Connection Authentication - - - Connection Details - - - Connection Dialog - - - Connection Error - - - Connection Failed - - - Connection Group - - - Connection Profile could not be updated. Please modify the connection details manually in settings.json and try again. - - - Connection String - - - Connection Timeout - - - Connection error - - - Connection is not active. Please establish a connection before performing this action. - - - Connection not found for uri "{0}". - {0} is the uri - - - Connection profile '{0}' not found. - {0} is the profile ID - - - Connection sharing permission denied for extension: '{0}'. Use the permission management commands to change this. - {0} is the extension ID - - - Connection sharing permission is required for extension: '{0}' - {0} is the extension ID - - - Connection string is required - - - Connection with ID "{0}" not found. Please verify the connection ID exists. - {0} is the connection ID - - - Consider adding a name for this foreign key - - - Container Name - - - Container creation isn't supported on this system. ARM support will be available starting with the SQL Server 2025 CU1 container image. - - - Container does not exist. Would you like to remove the connection? - - - Container failed to start within the timeout period. Please wait a few minutes and try again. - - - Contains - - - Continue Editing - - - Copied - - - Copy - - - Copy As - - - Copy Headers - - - Copy as CSV - - - Copy as IN clause - - - Copy as INSERT INTO - - - Copy as JSON - - - Copy code and open webpage - - - Copy connection string to clipboard - - - Copy script - - - Copy with Headers - - - Copying results... - - - Cost - - - Count: {0} - {0} is the count - - - Count: {0} Distinct Count: {1} Null Count: {2} - {0} is the count, {1} is the distinct count, and {2} is the null count - - - Create - - - Create As Script - - - Create Connection Group - - - Create Connection Profile - - - Create Container - - - Create Database - - - Create Firewall Rule - - - Create Local SQL Container - - - Create New Connection Group - - - Create a Local Docker SQL Server - - - Create a SQL Server container in seconds—no manual steps required. Manage it easily from the MSSQL extension without leaving VS Code. - - - Create a SQL database in Fabric (Preview) - - - Create a new firewall rule - - - Create new firewall rule for {0} - {0} is the server name that the firewall rule will be created for - - - Creating Container - - - Creating SQL Database for workspace {0} - {0} is the workspace ID - - - Creating and starting your SQL Server container - - - Creating workspace with capacity {0} - {0} is the capacity ID - - - Credential Error: An error occurred while attempting to refresh account credentials. Please re-authenticate. - - - Currently signed in as: - - - Custom Zoom - - - DacFx service is not available - - - Data Type - - - Data automatically replicated to OneLake in real time with a SQL analytics endpoint. - - - Data type mismatch: '{0}' in column '{1}' incompatible with '{2}' in '{3}' - {0} is source data type -{1} is source column -{2} is target data type -{3} is target column - - - Data-tier Application File (.dacpac) - - - Database - - - Database - {0} - {0} is the database name - - - Database Description - - - Database Name - - - Database Name is required - - - Database Project - - - Database changed successfully - - - Database list - - - Database name - - - Database name is required - - - Default - - - Default Value - - - Definition - - - DefinitionRequestCompleted - - - DefinitionRequested - - - Delete - - - Delete Confirmation - - - Delete Contents - - - Delete saved connection - - - Deleting Container... - - - Deny - - - Deployment Failed - - - Deployment Name - - - Deployment in progress - - - Description - - - Description for the table. - - - Details - - - Developer-friendly transactional database using the Azure SQL Database Engine. - - - Disable intellisense and syntax error checking on current document - - - Disabled - - - Disconnect - - - Disconnect from connection '{0}'? - {0} is the connection ID - - - Disconnected on document "{0}" - {0} is the document name - - - Disconnected successfully - - - Disconnecting from connection '{0}' - {0} is the connection ID - - - Dismiss - - - Displays the data type name for the column - - - Displays the description of the column - - - Displays the unified data type (including length, scale and precision) for the column - - - Dissatisfied - - - Distinct Count: {0} - {0} is the distinct count - - - Do you mind taking a quick feedback survey about the MSSQL Extension for VS Code? - - - Do you want to always display query results in a new tab instead of the query pane? - - - Docker failed to start within the timeout period. Please manually start Docker and try again. - - - Docker is not installed or not in PATH. Please install Docker Desktop and try again. - - - Docker requires root permissions to run. Please run Docker with sudo or add your user to the docker group using sudo usermod -aG docker $USER. Then, reboot your machine and retry. - - - Don't Show Again - - - Easily set up a local SQL Server without leaving VS Code extension. Just a few clicks to install, configure, and manage your server effortlessly! - - - Edit - - - Edit Connection Group - {0} - {0} is the connection group name - - - Edit Connection Group: {0} - {0} is the name of the connection group being edited - - - Edit Connection Profile - - - Edit Table - - - Either profileId or serverName must be provided. - - - Enable 'Trust Server Certificate' - - - Enable Experiences & Reload - - - Enable Trust Server Certificate - - - Enabled - - - Encountering a problem? Share the details with us by opening a GitHub issue so we can improve! - - - Encrypt - - - Encryption was enabled on this connection; review your SSL and certificate configuration for the target SQL Server, or enable 'Trust server certificate' in the connection dialog. - - - Encryption was enabled on this connection; review your SSL and certificate configuration for the target SQL Server, or set 'Trust server certificate' to 'true' in the settings file. Note: A self-signed certificate offers only limited protection and is not a recommended practice for production environments. Do you want to enable 'Trust server certificate' on this connection and retry? - - - End IP Address - - - Ends With - - - Enter Database Description - - - Enter Database Name - - - Enter connection group name - - - Enter container name - - - Enter description (optional) - - - Enter desired column width in pixels - - - Enter hostname - - - Enter new column width - - - Enter new password - - - Enter part of an object name to search for - - - Enter password - - - Enter port - - - Enter profile name - - - Entra token cache cleared successfully. - - - Equals - - - Error - - - Error Message: - - - Error code: - - - Error connecting to server "{0}". Details: {1} - {0} is the server name -{1} is the error message - - - Error connecting to: - - - Error creating firewall rule {0}. Check your Azure account settings and try again. Error: {1} - {0} is the rule info in format 'name (startIp - endIp)' -{1} is the error message - - - Error generating text view. Please try switching back to grid view. - - - Error loading Azure account information for tenant ID '{0}' - {0} is the tenant ID - - - Error loading Azure databases for subscription {0} ({1}). Confirm that you have permission. - {0} is the subscription name -{1} is the subscription id - - - Error loading Azure databases. - - - Error loading Azure subscriptions. - - - Error loading databases - - - Error loading designer - - - Error loading preview - - - Error loading summary - - - Error loading summary: {0} - {0} is the error message - - - Error loading workspaces - - - Error loading workspaces. Please try choosing a different account or tenant. - - - Error loading; refresh to try again - - - Error migrating connection ID {0} to new format. Please recreate this connection to use it. Error: {1} - {0} is the connection id -{1} is the error message - - - Error occurred opening content in editor. - - - Error retrieving server list: {0} - {0} is the error message - - - Error running Docker commands. Please make sure Docker is running. - - - Error signing into Azure: {0} - {0} is the error message - - - Error when refreshing token - - - Error {0}: {1} - {0} is the error number -{1} is the error message - - - Error {0}: {1} Please login as a different user and change the password using ALTER LOGIN. - {0} is the error number -{1} is the error message - - - Error: - - - Error: Login failed. Retry using different credentials? - - - Error: Unable to connect using the connection information provided. Retry profile creation? - - - Excel - - - Execute - - - Executing query... - - - Execution Plan - - - Existing Azure SQL logical server - - - Existing SQL server - - - Expand - - - Expand All - - - Expand Workspace Explorer - - - Expand properties pane - - - Explore, design, and evolve database schemas using intelligent, code-first or data-first guidance - - - Explorer - - - Export - - - Expression - - - Extremely likely - - - Fabric API error occurred ({0}): {1} - {0} is the error code -{1} is the error message - - - Fabric Account - - - Fabric Account is required - - - Fabric Workspaces - - - Fabric is not supported in the current cloud ({0}). Ensure setting '{1}' is configured correctly. - {0} is the cloud name -{1} is the setting name - - - Fabric long-running API error with error code '{0}': {1} - {0} is the error code -{1} is the error message - - - Failed - - - Failed disposing query: {0} - {0} is the error message - - - Failed to apply changes: '{0}' - {0} is the error message returned from the publish changes operation - - - Failed to change database - - - Failed to connect - - - Failed to connect to database: {0} - {0} is the database name - - - Failed to connect to server. - - - Failed to connect: {0} - {0} is the error message - - - Failed to delete credential with id: {0}. {1} - {0} is the id -{1} is the error - - - Failed to delete {0}. - Failed to delete {0}. - - - Failed to establish connection with ID "{0}". Please check connection details and network connectivity. - {0} is the connection ID - - - Failed to fetch user tokens. - - - Failed to generate publish script: '{0}' - {0} is the error message returned from the generate script operation - - - Failed to generate script: '{0}' - {0} is the error message returned from the generate script operation - - - Failed to get Fabric workspaces for tenant '{0} ({1})'. - {0} is the tenant name -{1} is the tenant id - - - Failed to get Fabric workspaces for tenant '{0} ({1})': {2} - {0} is the tenant name -{1} is the tenant id -{2} is the error message - - - Failed to get authentication method, please remove and re-add the account. - - - Failed to get tenant '{0}' for account '{1}'. - {0} is the tenant id -{1} is the account name - - - Failed to list databases - - - Failed to load publish profile - - - Failed to open scmp file: '{0}' - {0} is the error message returned from the open scmp operation - - - Failed to pull SQL Server image. Please check your network connection and try again. - - - Failed to refresh connection ${0} with uri {1}, invalid connection result. - {0} is the connection id -{1} is the uri - - - Failed to save publish profile - - - Failed to save results. - - - Failed to save scmp file: '{0}' - {0} is the error message returned from the save scmp operation - - - Failed to start SQL Server container. Please check the error message for more details, and then try again. - - - Failed to start query. - - - Failed to start {0}. - Failed to start {0}. - - - Failed to stop {0}. - Failed to stop {0}. - - - Favorites - - - Fetching {0} script... - {0} is the script type - - - File - - - Filter - - - Filter ({0}) - {0} is the number of selected tables - - - Filter Azure subscriptions - - - Filter Options - - - Filter Settings - - - Filter by keyword - - - Filter by type - - - Filter for any field... - - - Find - - - Find Next - - - Find Node - - - Find Nodes - - - Find Previous - - - Finish - - - Finished Deployment - - - Finished query execution for document "{0}" - {0} is the document name - - - Firewall rule name - - - Firewall rule successfully added. Retry profile creation? - - - Firewall rule successfully created. - - - Flat - - - Folder Structure - - - For numeric data, the maximum number of decimal digits that can be stored in this database object to the right of decimal point. - - - For numeric data, the maximum number of decimal digits that can be stored in this database object. - - - Foreign Column - - - Foreign Key - - - Foreign Key {0} - {0} is the index of the foreign key - - - Foreign Keys - - - Foreign Table - - - Foreign key '{0}' already exists - {0} is the foreign key name - - - Formula - - - Found pending reconnect promise for uri {0}, failed. - {0} is the uri - - - Found pending reconnect promise for uri {0}, waiting. - {0} is the uri - - - Found {0} saved connection profile(s). - {0} is the number of connection profiles - - - General - - - General Options - - - Generate Script - - - Generate mock data and seed scripts to support testing and development environments - - - Generate script to deploy changes to target - - - Generating Report. This may take a while... - - - Generating report, this might take a while... - - - Get Connection Details - - - Get Started - - - Get connection details for connection '{0}'? - {0} is the connection ID - - - Get security-related recommendations, such as avoiding SQL injection or excessive permissions - - - Getting Docker Ready... - - - Getting Fabric database '{0}' - {0} is the database ID - - - Getting Fabric workspace '{0}' - {0} is the workspace ID - - - Getting connection details for connection '{0}' - {0} is the connection ID - - - Getting connection string for SQL Endpoint '{0}' in workspace '{1}' - {0} is the SQL endpoint ID -{1} is the workspace ID - - - Getting container ready for connections - - - Getting definition ... - - - Got invalid tool use parameters: "{0}". ({1}) - {0} is the part input -{1} is the error message - - - Greater Than - - - Greater Than or Equals - - - Grid View - - - Help - - - Hide Confirm Password - - - Hide New Password - - - Hide full error message - - - Hide password - - - Hide this panel - - - Highlight Expensive Operation - - - Hostname - - - How likely it is that you would recommend the MSSQL extension to a friend or colleague? - - - How likely it is that you would recommend {0} to a friend or colleague? - {0} is the feature name - - - How many tables are in this database? - - - Hue - - - I have read the summary and understand the potential risks. - - - I'm sorry, I can only assist with SQL-related questions. - - - Ignore Tenant - - - Image tag - - - Importance - - - In progress - - - Include - - - Include Object Types - - - Include all object types - - - Index - - - Indexes - - - Initializing comparison, this might take a while... - - - Insert - - - Install Docker - - - Instant Container Setup - - - Insufficient Workspace Permissions - - - Integrated - - - Integrated & secure - - - Invalid Firewall rule name - - - Invalid IP Address - - - Invalid SQL Server password for {0}. Password must be 8–128 characters long and meet the complexity requirements. For more information see https://docs.microsoft.com/sql/relational-databases/security/password-policy - - - Invalid column width - - - Invalid connection URI provided. - - - Invalid connection URI. Please ensure you have an active database connection. - - - Invalid connection string: {0} - - - Invalid max length '{0}' - {0} is the max length - - - Is Computed - - - Is Identity - - - Is Persisted - - - Issues ({0}) - {0} is the number of issues - - - JPEG - - - JSON - - - Keep in query pane - - - Keys for token cache could not be saved in credential store, this may cause Microsoft Entra Id access token persistence issues and connection instabilities. It's likely that SqlTools has reached credential storage limit on Windows, please clear at least 2 credentials that start with "Microsoft.SqlTools|" in Windows Credential Manager and reload. - - - Learn More - - - Learn more about SQL Server 2025 features - - - Length - - - Length mismatch: Column '{0}' ({1}) incompatible with '{2}' ({3}) - {0} is source column -{1} is source length -{2} is target column -{3} is target length - - - Less Than - - - Less Than or Equals - - - Line {0} - {0} is the line number - - - List Connections - - - List Databases - - - List Functions - - - List Schemas - - - List Tables - - - List Views - - - List all connections registered with the mssql extension? - - - List databases for connection '{0}'? - {0} is the connection ID - - - List functions for connection '{0}'? - {0} is the connection ID - - - List schemas for connection '{0}'? - {0} is the connection ID - - - List tables for connection '{0}'? - {0} is the connection ID - - - List views for connection '{0}'? - {0} is the connection ID - - - Listing Fabric SQL Databases for workspace '{0}' - {0} is the workspace ID - - - Listing Fabric SQL Endpoints for workspace '{0}' - {0} is the workspace ID - - - Listing Fabric capacities for tenant '{0}' - {0} is the tenant ID - - - Listing Fabric workspaces for tenant '{0}' - {0} is the tenant ID - - - Listing databases for connection '{0}' - {0} is the connection ID - - - Listing functions for connection '{0}' - {0} is the connection ID - - - Listing role assignments for workspace '${workspaceId}' - {0} is the workspace ID - - - Listing schemas for connection '{0}' - {0} is the connection ID - - - Listing server connections - - - Listing tables for connection '{0}' - {0} is the connection ID - - - Listing views for connection '{0}' - {0} is the connection ID - - - Load - - - Load from Connection String - - - Load profile... - - - Load source, target, and options saved in an .scmp file - - - Loading - - - Loading Azure Accounts - - - Loading Fabric Accounts - - - Loading Report - - - Loading Schema Designer - - - Loading Schema Designer Model... - - - Loading Table Designer - - - Loading databases in '{0}'... - {0} is the name of the workspace - - - Loading databases in selected workspace... - - - Loading deployment - - - Loading execution plan... - - - Loading fabric provisioning... - - - Loading local containers... - - - Loading results... - - - Loading summary for {0} rows (Click to cancel) - {0} is the total number of rows - - - Loading tenants... - - - Loading text view... - - - Loading workspaces - - - Loading workspaces... - - - Loading... - - - Local SQL Server database container - - - Local development container - - - Location - - - Location (Workspace) - - - MSSQL - - - MSSQL - Azure Auth Logs - - - MSSQL Feedback - - - Manage Connection Profiles - - - Manage relationships - - - Mandatory (Recommended) - - - Mandatory (True) - - - Max Length - - - Max row count for filtering/sorting has been exceeded. To update it, navigate to User Settings and change the setting: mssql.resultsGrid.inMemoryDataProcessingThreshold - - - Max: {0} - {0} is the max - - - Maximize - - - Maximize panel size - - - Message - - - Messages - - - Metric - - - Microsoft Account - - - Microsoft Account is required - - - Microsoft Corp - - - Microsoft Entra Account - - - Microsoft Entra Id - - - Microsoft Entra Id - Universal w/ MFA Support - - - Microsoft Entra account {0} successfully added. - {0} is the account name - - - Microsoft SQL Server License Agreement - - - Microsoft reviews your feedback to improve our products, so don't share any personal data or confidential/proprietary content. - - - Microsoft will process the feedback you submit pursuant to your organization’s instructions in order to improve your and your organization’s experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. - - - Microsoft will process the feedback you submit pursuant to your organization’s instructions in order to improve your and your organization’s experience with this product. If you have any questions... - - - Microsoft would like your feedback - - - Min: {0} - {0} is the min - - - Move Down - - - Move Up - - - Move to Root - - - My Data - - - NULL - - - Name - - - Name of the primary key. - - - New Azure SQL logical server (Preview) - - - New Check Constraint - - - New Column - - - New Column Mapping - - - New Deployment - - - New Foreign Key - - - New Index - - - New Microsoft Entra account could not be added. - - - New Password - - - New SQL Server local development container - - - New SQL database - - - New column mapping - - - Next - - - No - - - No Action - - - No Microsoft Entra account can be found for removal. - - - No Queries Available - - - No account selected - - - No active connection - - - No active connection for database change - - - No active connection for schema view - - - No active database connection in the current editor. Please establish a connection to continue. - - - No active database connection. Please connect to a database first. - - - No active text editor found. Please open a file with an active database connection. - - - No changes detected - - - No connection credentials found - - - No connection found for connectionId: {0} - {0} is the connection ID - - - No connection information found - - - No connection profile to remove. - - - No connection was found. Please connect to a server first. - - - No database objects found matching '{0}' - {0} is the search term - - - No databases found in the selected workspace. - - - No databases found in workspace '{0}'. - {0} is the name of the workspace - - - No items - - - No model found. - - - No need to refresh Microsoft Entra acccount token for connection {0} with uri {1} - {0} is the connection id -{1} is the uri - - - No results - - - No results for the active editor - - - No results to display - - - No saved connection profiles found. - - - No schema differences were found. - - - No subscriptions available. Adjust your subscription filters to try again. - - - No tenant selected - - - No tools to process. - - - No workspaces found - - - No workspaces found. Please change Fabric account or tenant to view available workspaces. - - - Non-SQL Server SQL file detected. Disable IntelliSense for such files? - - - None - - - Not Between - - - Not Contains - - - Not Ends With - - - Not Equals - - - Not Starts With - - - Not likely at all - - - Not signed in - - - Not started - - - Note: A self-signed certificate offers only limited protection and is not a recommended practice for production environments. Do you want to enable 'Trust server certificate' on this connection and retry? - - - Null Count: {0} - {0} is the null count - - - Number of Rows Read - - - OK - - - OLTP, built on Azure SQL - - - Object Explorer Filter - - - Object Type - - - Off - - - On Delete - - - On Delete Action - - - On Update - - - On Update Action - - - Open - - - Open .scmp file - - - Open Publish Script - - - Open Query - - - Open Query History - - - Open SQL editor and connect - - - Open XML - - - Open in Editor - - - Open in New Tab - - - Open in editor - - - Opening Publish Script. This may take a while... - - - Opening schema designer... - - - Operator - - - Option Description - - - Optional (False) - - - Options - - - Options have changed. Recompare to see the comparison? - - - Overall, how satisfied are you with the MSSQL extension? - - - Overall, how satisfied are you with {0}? - {0} is the feature name - - - PNG - - - Parameters - - - Parent node was not TreeNodeInfo. - - - Password - - - Password (SQL Login) - - - Password is required - - - Password must be changed for '{0}' to continue logging into '{1}' - {0} is the username -{1} is the name of the server - - - Password must be changed to continue logging into '{0}' - {0} is the name of the server - - - Passwords do not match - - - Paste - - - Paste connection string from clipboard - - - Path: {0} - {0} is the path of the node in the object explorer - - - Pick from multiple SQL Server versions, including SQL Server 2025 (Preview) with built-in AI capabilities like vector search and JSON enhancements. - - - Please Accept the SQL Server EULA - - - Please choose a unique name for the container - - - Please choose a unique name for the profile - - - Please make sure the port is a number, and choose a port that is not in use - - - Please make your password 8-128 characters long. - - - Please select a workspace where you have sufficient permissions (Contributor or higher) - - - Port - - - Port must be a number between 1 and 65535 - - - Possible Data Loss detected. Please review the changes. - - - Precision - - - Precision/scale mismatch between '{0}' and '{1}' - {0} is source column -{1} is target column - - - Preview Database Updates - - - Previous - - - Previous pending reconnect promise for uri {0} is rejected with error {1}, will attempt to reconnect if necessary. - {0} is the uri -{1} is the error - - - Previous pending reconnection for uri {0}, succeeded. - {0} is the uri - - - Previous step failed. Please check the error message and try again. - - - Primary Key - - - Primary Key Columns - - - Privacy Statement - - - Processing include or exclude all differences operation. - - - Profile Name - - - Profile created and connected - - - Profile created successfully - - - Profile removed successfully - - - Properties - - - Property - - - Provider '{0}' does not have a Microsoft resource endpoint defined. - {0} is the provider - - - Provisioning - - - Publish - - - Publish Changes - - - Publish Profile - - - Publish Project - - - Publish Settings File - - - Publish Target - - - Publish profile saved to: {0} - - - Publishing Changes - - - Pulling SQL Server Image - - - Pulling the SQL Server container image. This might take a few minutes depending on your internet connection. - - - Query Plan ({0}) - {0} is the number of query plans - - - Query executed - - - Query failed - - - Query succeeded - - - Query {0}: Query cost (relative to the script): {1}% - {0} is the query number -{1} is the query cost - - - Read more - - - Readying container for connections. - - - Readying container for connections... - - - Receive natural language explanations to help developers unfamiliar with T-SQL understand code - - - Recent - - - Recent Connections - - - Recent connections list cleared - - - Redo - - - Referenced column '{0}' not found - {0} is the column name - - - Referenced table '{0}' not found - {0} is the table name - - - Refresh - - - Refresh Credentials - - - Reload Visual Studio Code - - - Remind Me Later - - - Remove - - - Remove Sort - - - Remove recent connection - - - Remove {0} - {0} is the object type - - - Replication - - - Required - - - Reset - - - Resize - - - Resize column '{0}' - {0} is the name of the column - - - Resource Group - - - Restore - - - Restore panel size - - - Restore properties pane - - - Result Set {0} - {0} is the index of the result set - - - Results - - - Results ({0}) - {0} is the number of results - - - Results copied to clipboard - - - Retry - - - Reverse Alphabetical - - - Reverse-engineer existing databases by explaining SQL schemas and relationships - - - Rosetta is required to run SQL Server container images on Apple Silicon. Enable "Use Rosetta for x86_64/amd64 emulation on Apple Silicon" in Docker Desktop > Settings > General. - - - Rule name - - - Run Query - - - Run Query History - - - Run a query in the current editor, or switch to an editor that has results. - - - Run query on connection '{0}'? Query: {1} - {0} is the connection ID -{1} is the SQL query - - - Running query is not supported when the editor is in multiple selection mode. - - - Running query on connection '{0}' - {0} is the connection ID - - - SQL Analytics Endpoint - - - SQL Container Name - - - SQL Container Version - - - SQL Database - - - SQL Login - - - SQL Plan Files - - - SQL Server 2025 is not supported on ARM architecture. Please select a different SQL Server version. - - - SQL Server 2025 is not yet supported on ARM architecture. ARM support will be available starting with the SQL Server 2025 CU1 container image. - - - SQL Server Container SA Password - - - SQL Server admin password - - - SQL Server does not support Windows containers. Please switch to Linux containers in Docker Desktop settings. - - - SQL Server port number - - - SQL Server {0} - latest - {0} is the SQL Server version - - - SQL database in Fabric (Preview) - - - SQLCMD Variables - - - SVG - - - Satisfied - - - Saturation - - - Save - - - Save .scmp file - - - Save As - - - Save As... - - - Save Connection Group - - - Save Password - - - Save Password? If 'No', password will be required each time you connect - - - Save Plan - - - Save as CSV - - - Save as Excel - - - Save as INSERT - - - Save as JSON - - - Save results command cannot be used with multiple selections. - - - Save source and target, options, and excluded elements - - - Saved Connections - - - Scaffold backend components (e.g., data-access layers) based on your current database context - - - Scale - - - Scale mismatch between '{0}' and '{1}' - {0} is source column -{1} is target column - - - Schema - - - Schema Compare - - - Schema Compare Options - - - Schema Compare failed: '{0}' - {0} is the error message returned from the compare operation - - - Schema Designer - - - Schema Designer Model is ready. Changes can now be published. - - - Schema visualization opened. - - - Schema/Object Type - - - Script - - - Script As Create - - - Script copied to clipboard - - - Search Workspaces - - - Search connection groups - - - Search for database objects... - - - Search options... - - - Search settings... - - - Search tables... - - - Search term cannot be empty - - - Search workspaces... - - - Search... - - - See - - - Select - - - Select All - - - Select Azure account with Key Vault access for column decryption - - - Select Connection - - - Select Profile - - - Select Source - - - Select Source Schema - - - Select Target - - - Select Target Schema - - - Select a Workspace - - - Select a connection group - - - Select a tenant - - - Select a valid {0} from the dropdown - {0} is the type of the dropdown's contents, e.g 'resource group' or 'server' - - - Select a workspace to view the databases in it. - - - Select a {0} for filtering - {0} is the type of the dropdown's contents, e.g 'resource group' or 'server' - - - Select all - - - Select all options - - - Select an account - - - Select an extension to manage connection sharing permissions - - - Select an object to view its definition ({0} results) - {0} is the number of results - - - Select image - - - Select new permission for extension: '{0}' - {0} is the extension name - - - Select profile to remove - - - Select subscriptions - - - Select the SQL Server Container Image - - - Selected Microsoft Entra account removed successfully. - - - Server - - - Server - {0} - {0} is the server name - - - Server Edition - - - Server Version - - - Server connection in progress. Do you want to cancel? - - - Server could not start. This could be a permissions error or an incompatibility on your system. You can try enabling device code authentication from settings. - - - Server is required - - - Server name not set. - - - Server name or ADO.NET connection string - - - Server {0} not found. - {0} is the server name - - - Set Default - - - Set Null - - - Setting up - - - Setting up container - - - Settings - - - Severity - - - Show All - - - Show Confirm Password - - - Show MSSQL output - - - Show Menu (F3) - - - Show New Password - - - Show Schema - - - Show a random table definition - - - Show full error message - - - Show password - - - Show schema for connection '{0}'? - {0} is the connection ID - - - Show table relationships - - - Showing schema for connection '{0}' - {0} is the connection ID - - - Showplan XML - - - Sign In - - - Sign in - - - Sign in to a new account - - - Sign in to your Azure subscription - - - Sign in to your Azure subscription in one of the sovereign clouds. - - - Sign in to your Azure subscription with a device code. Use this in setups where the Sign In command does not work - - - Sign into Azure - - - Sign into Azure in order to add a firewall rule. - - - Sign into Fabric - - - Signing in to Azure... - - - Simple Container Management - - - Smart performance - - - Sort - - - Sort Ascending - - - Sort Descending - - - Source - - - Source Column - - - Source Name - - - Specifies whether the column is included in the primary key for the table. - - - Specifies whether the column may have a NULL value. - - - Start IP Address - - - Start Time - - - Start, stop, and remove containers directly from the extension. - - - Started executing query at - - - Started query execution for document "{0}" - {0} is the document name - - - Started saving results to - - - Starting Container... - - - Starting Docker... - - - Starting {0}... - {0} is the container name - - - Starts With - - - Stop - - - Stopping Container... - - - Submit - - - Submit an issue - - - Subscription - - - Subtree Cost - - - Succeeded - - - Succeeded with warning - - - Successfully changed to database: {0} - {0} is the database name - - - Successfully connected to server. - - - Successfully refreshed token for connection {0} with uri {1}, {2} - {0} is the connection id -{1} is the uri -{2} is the message - - - Successfully saved results to - - - Sum: {0} - {0} is the sum - - - Summary loading canceled - - - Summary loading was canceled by user - - - Switch Direction - - - Switch Source and Target - - - Switch to Grid View - - - Switch to MSAL - - - Switch to Text View - - - Switching to Linux containers was canceled. SQL Server only supports Linux containers. - - - Table - - - Table '{0}' already exists - {0} is the table name - - - Table '{0}' not found - {0} is the table name - - - Table name - - - Table name cannot be empty - - - Take Survey - - - Target - - - Target Name - - - Target Table - - - Tenant - - - Tenant ID - - - Tenant ID is required - - - Terms & Conditions - - - Test Connection - - - Testing connection profile... - - - Text View - - - The MSSQL for VS Code extension is introducing new modern data development features! Would you like to enable them? [Learn more]({0}) - {0} is a url to learn more about the new features - - - The SQL Server 2025 RTM container image isn't compatible with ARM-based systems (including Windows ARM and Apple Silicon). - - - The behavior when a user tries to delete a row with data that is involved in a foreign key relationship. - - - The behavior when a user tries to update a row with data that is involved in a foreign key relationship. - - - The columns of the index. - - - The connection with ID '{0}' does not have the 'server' property set and is being ignored. Please set the 'server' property on this connection in order to use it. - {0} is the connection ID for the connection that has been ignored - - - The custom cloud choice is not configured. Please configure the setting `{0}`. - - - The description of the check constraint. - - - The description of the foreign key. - - - The description of the index. - - - The description of the primary key. - - - The expression defining the check constraint. - - - The extension '{0}' is requesting access to your SQL Server connections. This will allow it to execute queries and access your database. - {0} is the extension name - - - The first value must be less than the second value for the {0} operator in the {1} filter - {0} is the operator for the filter -{1} is the name of the filter - - - The first value must be set for the {0} operator in the {1} filter - {0} is the operator for the filter -{1} is the name of the filter - - - The following workspace or workspace folder connections are missing the 'id' property and are being ignored. Please manually add the 'id' property to the connection in order to use it. {0} - {0} is the list of display names for the connections that have been ignored - - - The language model did not return any output. - - - The mapping between foreign key columns and primary key columns. - - - The maximum length (in characters) that can be stored in this database object. - - - The name of the check constraint. - - - The name of the column object. - - - The name of the column. - - - The name of the foreign key. - - - The name of the index. - - - The recent connections list has been cleared but there were errors while deleting some associated credentials. View the errors in the MSSQL output channel. - - - The requested model could not be found. Please check model availability or try a different model. - - - The second value must be set for the {0} operator in the {1} filter - {0} is the operator for the filter -{1} is the name of the filter - - - The table which contains the primary or unique key column. - - - There was an error updating the project - - - This database name is already in use. Please choose a different name. - - - This message couldn't be processed. If this issue persists, please check the logs and open an issue on GitHub. - - - Timestamp - - - To compare two schemas, first select a source schema and target schema, then press compare. - - - To use this command, Open a .sql file -or- Change editor language to "SQL" -or- Select T-SQL text in the active SQL editor. - - - To use this command, you must set the language to "SQL". Confirm to change language mode. - - - Toggle Tooltips - - - Tool lookup for: {0} - {1}. - {0} is the part name -{1} is the part input - - - Total execution time: {0} - {0} is the elapsed time - - - Type - - - Unable to execute the command while the extension is initializing. Please try again later. - - - Unable to expand. Please check logs for more information. - - - Unable to read proxy agent options to get tenants. - - - Understand and document business logic embedded in stored procedures, views, and functions - - - Undo - - - Unknown error - - - Unnamed Profile - - - Unsupported architecture for Docker: {0} - {0} is the architecture name of the machine - - - Unsupported platform for Docker: {0} - {0} is the platform name of the machine - - - Update - - - Update Database - - - Updating IntelliSense... - - - Usage limits exceeded. Try again later, or consider optimizing your requests. - - - Use T-SQL intellisense and syntax error checking on current document - - - Use {0} to create a new connection. - {0} is the connect command - - - User - - - User name - - - User name (SQL Login) - - - User name is required - - - Username - - - Using {0} ({1})... - {0} is the model name -{1} is whether the model can send requests - - - Using {0} to process your request... - {0} is the model name that will be processing the request - - - Value - - - Very Dissatisfied - - - Very Satisfied - - - View More - - - View mssql for Visual Studio Code release notes? - - - Visual Studio Code must be relaunched for this setting to come into effect. Please reload Visual Studio Code. - - - Warning - - - Warnings detected. Please review the changes. - - - We can't find where Docker Desktop is located on your machine. Please manually start Docker Desktop and try again. - - - We couldn't connect using the current connection information. Would you like to retry the connection or edit the connection profile? - - - What I can do for you: - - - What can we do to improve? - - - Width cannot be 0 or negative - - - Windows Authentication - - - Works with VS Code/SSMS and uses Microsoft Entra authentication and Fabric access controls. - - - Workspace - - - Workspace is required - - - Write, optimize, and troubleshoot SQL queries with AI-recommended improvements - - - Yes - - - You are not connected to any database. - - - You must accept the license - - - You must be signed into Azure in order to browse SQL databases. - - - You must be signed into Fabric in order to browse SQL databases. - - - You must review and accept the terms to proceed - - - Your Docker Engine currently runs Windows containers. SQL Server only supports Linux containers. Would you like to switch to Linux containers? - - - Your account needs re-authentication to access {0} resources. Press Open to start the authentication process. - {0} is the resource - - - Your client IP Address '{0}' does not have access to the server '{1}' you're attempting to connect to. Would you like to create new firewall rule? - {0} is the client IP address -{1} is the server name - - - Your client IP address does not have access to the server. Add a Microsoft Entra account and create a new firewall rule to enable access. - - - Your password must contain characters from at least three of the following categories: uppercase letters, lowercase letters, numbers (0-9), and special characters (!, $, #, %, etc.). - - - Your tenant '{0} ({1})' requires you to re-authenticate again to access {2} resources. Press Open to start the authentication process. - {0} is the tenant name -{1} is the tenant id -{2} is the resource - - - Zoom In - - - Zoom Out - - - Zoom to Fit - - - [Optional] Database to connect (press Enter to connect to <default> database) - - - [Optional] Enter a display name for this connection profile - - - authenticationType - - - database - - - default - - - delete the saved connection: {0}? - {0} is the connection name - - - encrypt - - - for more details - - - hostname\instance or <server>.database.windows.net or ADO.NET connection string - - - intelliSenseUpdated - - - location - - - macOS Sierra or newer is required to use this feature. - - - resource group - - - server - - - subscription - - - test - - - untitled - - - updatingIntelliSense - - - {0} (Current Account) - {0} is the account display name - - - {0} (filtered) - - - {0} accounts - {0} is the number of accounts - - - {0} column data - {0} is the number of columns - - - {0} deleted successfully. - {0} deleted successfully. - - - {0} errors - {0} is the number of errors - - - {0} has been closed. Would you like to restore it? - {0} is the webview name - - - {0} invalid Entra accounts have been removed; you may need to run `MS SQL: Clear Microsoft Entra account token cache` and log in again. - {0} is the number of invalid accounts that have been removed - - - {0} issue - {0} is the number of issues - - - {0} issues - {0} is the number of issues - - - {0} of {1} - {0} is the number of active elements -{1} is the total number of elements - - - {0} password doesn't match the confirmation password - - - {0} properties - {0} is the object type - - - {0} rows selected, click to load summary - {0} is the number of rows to fetch summary statistics for - - - {0} selected - {0} is the number of selected rows - - - {0} started successfully. - {0} started successfully. - - - {0} stopped successfully. - {0} stopped successfully. - - - {0} warnings - {0} is the number of warnings - - - {0} {1} issue - {0} is the tab name -{1} is the number of issues - - - {0} {1} issues - {0} is the tab name -{1} is the number of issues - - - {0}. {1} - {0} is the status -{1} is the message - - - {0}: {1} - {0} is the task name -{1} is the status - - - {0}: {1}. {2} - {0} is the task name -{1} is the status -{2} is the message - - - {{put-server-name-here}} - - - ✅ Grant Access - - - ✅ Grant Access (Current) - - - ❌ Deny Access - - - ❌ Deny Access (Current) - - - 👋 I'm GitHub Copilot for MSSQL extension, your intelligent SQL development assistant in Visual Studio Code. I help you connect, explore, design, and evolve your SQL databases directly from VS Code. - - - - - Add Connection - - - Add Connection Group - - - Add Microsoft Entra Account - - - Allows users to sign in to input-constrained devices. - - - Always Encrypted - - - An execution time-out of 0 indicates an unlimited wait (no time-out) - - - Analyze Query Performance - - - Application Intent - - - Automatically adjust the column widths based on the visible rows in the result set. Could have performance problems with a large number of columns or large cells - - - Automatically display query results in a new tab instead of the query pane. This option takes effect only if `mssql.enableRichExperiences` is enabled. - - - Automatically reveal the results panel when switching to an editor with query results. Only applies when 'Open Results in Tab' is enabled. - - - Azure MFA - - - Cancel Query - - - Change Connection - - - Change Database - - - Choose SQL handler for this file - - - Chooses which Authentication method to use - - - Clear All Connection Sharing Permissions - - - Clear All Query History - - - Clear Filters - - - Clear Microsoft Entra account token cache - - - Clear Pooled Connections - - - Command Timeout - - - Connect - - - Connect to a SQL Database - - - Connection Dialog - - - Connection Timeout - - - Connection groups - - - Connection profiles defined in 'User Settings' are shown under 'MS SQL: Connect' command in the command palette. - - - Connections - - - Controls the max number of rows allowed to do filtering and sorting in memory. If the number is exceeded, sorting and filtering will be disabled. Warning: Increasing this may impact performance. - - - Copy All - - - Copy Object Name - - - Create Azure Function with SQL binding - - - Create a Table with Table Designer - - - Create a new table in your database, or edit existing tables with the table designer. Once you're done making your changes, click the 'Publish' button to send the changes to your database. - - - Default view mode for query results display. - - - Delete - - - Delete Connection Group - - - Delete Container - - - Disable Actual Plan - - - Disable Group By Schema - - - Disabled - - - Disconnect - - - Display results in a formatted text format. - - - Display results in a tabular grid format (default) - - - Do not show prompts to display query results in a new tab. - - - Do not show prompts to enable UI-based features - - - Edit Connection - - - Edit Connection Group - - - Edit Connection Sharing Permissions - - - Edit Table - - - Enable Actual Plan - - - Enable Group By Schema - - - Enable Modern Features - - - Enable Modern Features - - - Enable Parameterization for Always Encrypted - - - Enable Query History Capture - - - Enable SET ANSI_DEFAULTS - - - Enable SET ANSI_NULLS - - - Enable SET ANSI_NULL_DFLT_ON - - - Enable SET ANSI_PADDING - - - Enable SET ANSI_WARNINGS - - - Enable SET ARITHABORT option - - - Enable SET CURSOR_CLOSE_ON_COMMIT - - - Enable SET DEADLOCK_PRIORITY option - - - Enable SET IMPLICIT_TRANSACTIONS - - - Enable SET LOCK TIMEOUT option (in milliseconds) - - - Enable SET NOCOUNT option - - - Enable SET NOEXEC option - - - Enable SET PARSEONLY option - - - Enable SET QUERY_GOVERNOR_COST_LIMIT - - - Enable SET QUOTED_IDENTIFIER - - - Enable SET STATISTICS IO option - - - Enable SET STATISTICS TIME option - - - Enable SET TRANSACTION ISOLATION LEVEL option - - - Enable SET XACT_ABORT ON option - - - Enable expand/collapse buttons in Schema Designer table nodes when tables have more than 10 columns - - - Enable modern features in MSSQL - - - Enable the new set of data development features that provide a modern way to work with your SQL database in VS Code. [Enable New Experiences](command:mssql.enableRichExperiences) - - - Enabled - - - Enables UI-based features in the MSSQL extension for richer and more powerful features. Restart Visual Studio Code after changing this setting. - - - Enables connection pooling to improve overall connectivity performance. This setting is disabled by default. Visual Studio Code is required to be relaunched when the value is changed. To clear pooled connections, run the command: 'MS SQL: Clear Pooled Connections'. Note: May keep serverless databases active and prevent auto-pausing. - - - Enables experimental features in the MSSQL extension. The features are not production-ready and may have bugs or issues. Restart Visual Studio Code after changing this setting. - - - Enables use of the Sql Authentication Provider for 'Microsoft Entra Id Interactive' authentication mode when user selects 'AzureMFA' authentication. This enables Server-side resource endpoint integration when fetching access tokens. This option is only supported for 'MSAL' Authentication Library. Please restart Visual Studio Code after changing this option. - - - Estimated Plan - - - Execute Current Statement - - - Execute Query - - - Explain Query - - - Familiarize yourself with more features of the MSSQL extension that can help you be more productive. - - - Filter - - - Filter your Object Explorer Tree - - - Get Started with MSSQL for Visual Studio Code - - - Getting Started Guide - - - MSSQL Copilot - - - MSSQL configuration - - - Make a new connection to a SQL database, or edit existing connections with the connection dialog. You can connect to a database by entering your connection information, using a connection string, or browsing your Azure subscriptions. [Open Connection Dialog](command:mssql.addObjectExplorer) - - - Manage Connection Profiles - - - Maximum number of characters to store for each value in XML columns after running a query. Default value: 2,097,152. Valid value range: 1 to 2,147,483,647. - - - Maximum number of characters/bytes to store for each value in character/binary columns after running a query. Default value: 65,535. Valid value range: 1 to 2,147,483,647. - - - Maximum number of old files to remove upon startup that have expired mssql.logRetentionMinutes. Files that do not get cleaned up due to this limitation get cleaned up next time Azure Data Studio starts up. - - - Maximum number of rows to return before the server stops processing your query. - - - Maximum size of text and ntext data returned from a SELECT statement - - - New Deployment - - - New Query - - - New Query - - - New Table - - - Next Steps with MSSQL for Visual Studio Code - - - Number of minutes to retain log files for backend services. Default is 1 week. - - - Number of query history entries to show in the Query History view - - - Object Explorer filters - - - Only see the database objects that matter most to you by applying filters to the Object Explorer tree. Start by clicking the filter button next to most folders in the Connections view. - - - Open Execution Plan File - - - Open New Query and Connect - - - Open Query - - - Open Query History in Command Palette - - - Open in Copilot Agent mode - - - Open in Copilot Ask mode - - - Pause Query History Capture - - - Prevent automatic execution of scripts (e.g., 'Select Top 1000'). When enabled, scripts will not be automatically executed upon generation. - - - Prompts users to sign in using their browser. - - - Query History - - - Query Results - - - Query editor and query results pane - - - Query plan visualization - - - Refresh - - - Refresh IntelliSense Cache - - - Remove - - - Remove Microsoft Entra Account - - - Replication - - - Reveal Query Result - - - Rewrite Query - - - Run Query - - - Run a SQL Query - - - SQL Container Name - - - SQL Container Version - - - Schema Compare - - - Schema Designer - - - Script as Alter - - - Script as Create - - - Script as Drop - - - Script as Execute - - - Select Top 1000 - - - Selected Azure subscriptions for browsing and managing servers and databases - - - Send Feedback - - - Set the font family for the results grid; set to blank to use the editor font - - - Set the font size for the results grid; set to blank to use the editor size - - - Shortcuts related to the results window - - - Should BIT columns be displayed as numbers (1 or 0)? If false, BIT columns will be displayed as 'true' or 'false' - - - Should IntelliSense be enabled - - - Should IntelliSense error checking be enabled - - - Should IntelliSense quick info be enabled - - - Should IntelliSense suggestions be enabled - - - Should IntelliSense suggestions be lowercase - - - Should Personally Identifiable Information (PII) be logged in the Azure Logs output channel and the output channel log file. - - - Should Query History feature be enabled - - - Should column definitions be aligned? - - - Should data types be formatted as UPPERCASE, lowercase, or none (not formatted) - - - Should keywords be formatted as UPPERCASE, lowercase, or none (not formatted) - - - Should language service be auto-disabled when extension detects Non-MSSQL files - - - Should query result selections and scroll positions be saved when switching tabs (may impact performance) - - - Should references to objects in a select statements be split into separate lines? E.g. for 'SELECT C1, C2 FROM T1' both C1 and C2 will be on separate lines - - - Show the active SQL connection details as a CodeLens suggestion at the top of the editor for quick visibility. - - - Sort and Filter Query Results - - - Sort and filter options for query results - - - Sort and filter your query results to find the data you need quickly. - - - Sovereign cloud equivalent for `.database.fabric.microsoft.com` (including leading dot) - - - Sovereign cloud equivalent for `.database.windows.net` (including leading dot) - - - Sovereign cloud equivalent for `.datawarehouse.fabric.microsoft.com` (including leading dot) - - - Sovereign cloud equivalent for `.sql.azuresynapse.net` (including leading dot) - - - Sovereign cloud equivalent for `https://analysis.windows.net/powerbi/api/` - - - Sovereign cloud equivalent for `https://api.fabric.microsoft.com/v1/` - - - Sovereign cloud equivalent for `https://database.windows.net/` - - - Sovereign cloud equivalent for `https://vault.azure.net/` - - - Start Container - - - Start Query History Capture - - - Stop Container - - - Table Designer - - - Temporarily store passwords for connections with 'Saved Passwords' disabled, until the extension is restarted. This prevents repeated password prompts when reusing connections within the same session. - - - The additional, MSSQL-specific custom configuration for the Sovereign Cloud to use with the Microsoft Sovereign Cloud authentication provider. This along with setting `microsoft-sovereign-cloud.environment` to `custom` and providing values for `microsoft-sovereign-cloud.customEnvironment` is required to use this feature with MSSQL. - - - The color of the connection group. - - - The description of the connection group. - - - The maximum number of characters to display for the connection info in the status bar. Set to -1 for no limit. - - - The maximum number of recently used connections to store in the connection list. - - - The name of the connection group. - - - The timeout in seconds for expanding a node in Object Explorer. The default value is 45 seconds. - - - The unique identifier for the connection group this connection profile belongs to. - - - The unique identifier for the connection group. - - - The unique identifier for the parent connection group. - - - The unique identifier for this connection profile. - - - This setting will be removed in a future release. - - - Toggle SQLCMD Mode - - - True for the messages pane to be open by default; false for closed - - - Understand what your query is doing by viewing the query plan. See the estimated plan without running the query, or view the actual query plan after running the query by toggling the buttons at the top of a query editor window. - - - Use Database - - - Visualize a Query Plan - - - When enabled, colorizes the connection status bar item with the color of the connection group. This setting is disabled by default. This uses the connection group folder's color directly, and does not alter it in order to ensure contrast with the current VS Code theme. Users should choose connection group colors that work well with their theme. - - - When enabled, connection groups will be collapsed instead of expanded at startup. - - - When enabled, the database objects in Object Explorer will be categorized by schema. - - - Windows Authentication - - - Write a SQL query, and run it against your database. You can also click the 'Open in New Tab' button to view your query results in their own tab, and optionally set that as the default behavior. - - - Your first steps for connecting to and developing with a SQL database - - - [Optional] Character used for enclosing text fields when saving results as CSV - - - [Optional] Character(s) used for separating rows when saving results as CSV - - - [Optional] Configuration options for copying multi-line results from the Results View - - - [Optional] Configuration options for copying results from the Results View - - - [Optional] Configuration options for which column new result panes should open in - - - [Optional] Declares the application workload type when connecting to SQL Server such as ReadWrite or ReadOnly. Refer to SQL Server AlwaysOn for more detail. - - - [Optional] Delimiter for separating data items when saving results as CSV. Choose from common separators like comma (,), tab (\t), semicolon (;), or pipe (|) - - - [Optional] Do not show unsupported platform warnings - - - [Optional] File encoding used when saving results as CSV. Choose from UTF-8 (recommended), UTF-16, ASCII, or Latin-1 based on your target application compatibility - - - [Optional] Indicates the SQL Server language settings. - - - [Optional] Indicates the name of local docker container the connection is on - - - [Optional] Indicates whether this profile has an empty password explicitly set - - - [Optional] Indicates which server type the provider will expose through the DataReader. - - - [Optional] Log debug output to the VS Code console (Help -> Toggle Developer Tools) - - - [Optional] Log level for backend services. Azure Data Studio generates a file name every time it starts and if the file already exists the logs entries are appended to that file. For cleanup of old log files see logRetentionMinutes and logFilesRemovalLimit settings. The default tracingLevel does not log much. Changing verbosity could lead to extensive logging and disk space requirements for the logs. Error includes Critical, Warning includes Error, Information includes Warning and Verbose includes Information - - - [Optional] Should execution time be shown for individual batches - - - [Optional] Specify a custom name for this connection profile to easily browse and search in the command palette of Visual Studio Code. - - - [Optional] Specify the SQL Server authentication type. - - - [Optional] Specify the database name to connect to. If database is not specified, the default user database setting is used, typically 'master'. - - - [Optional] Specify the delay between attempts to restore connection. - - - [Optional] Specify the length of time in seconds to wait for a command to execute before terminating the attempt and generating an error. The default value is 30 seconds. - - - [Optional] Specify the length of time in seconds to wait for a connection to the server before terminating connection attempt and generating an error. The default value is 30 seconds. - - - [Optional] Specify the maximum number of connections allowed in the pool. - - - [Optional] Specify the minimum amount of time in seconds for this connection to live in the pool before being removed/deleted. - - - [Optional] Specify the minimum number of connections allowed in the pool. - - - [Optional] Specify the name of the application used for SQL Server to log (default: 'vscode-mssql'). - - - [Optional] Specify the name of the primary file, including the full path name, of an attachable database. - - - [Optional] Specify the name of the workstation connecting to SQL Server. - - - [Optional] Specify the name or network address of the instance of SQL Server that acts as a failover partner. - - - [Optional] Specify the number of attempts to restore connection. - - - [Optional] Specify the password for SQL Server authentication. If password is not specified or already saved, when you connect, you will be asked again. - - - [Optional] Specify the port number to connect to. - - - [Optional] Specify the size in bytes of the network packets to communicate with SQL Server. - - - [Optional] Specify the user name for SQL Server authentication. If user name is not specified, when you connect, you will be asked again. - - - [Optional] The ADO.NET connection string to use for the connection. Overrides any other options given in this connection. - - - [Optional] Used by SQL Server in replication. - - - [Optional] When 'Mandatory' or 'Strict', SQL Server uses SSL encryption for all data sent between the client and server if the server has a certificate installed. When set to 'Strict', SQL Server uses TDS 8.0 for all data transfer between the client and server. 'Strict' is supported on SQL Server 2022 onwards. - - - [Optional] When set to 'true', multiple result sets can be returned and read from on connection. - - - [Optional] When set to 'true', the SQL Server SSL certificate is automatically trusted when the communication layer is encrypted using SSL. Set 'false' for Azure SQL Database connection. - - - [Optional] When set to 'true', the connection object is drawn from the appropriate pool, or if necessary, is created and added to the appropriate pool. Note: May keep serverless databases active and prevent auto-pausing. - - - [Optional] When set to 'true', the detection and connection to the active server is faster if AlwaysOn Availability Group is configured on different subnets. - - - [Optional] When set to 'true', the password for SQL Server authentication is saved in the secure store of your operating system such as KeyChain in MacOS or Secure Store in Windows. - - - [Optional] When set to false, security-sensitive information, such as the password, is not returned as part of the connection if the connection is open or has ever been in an open state. - - - [Optional] When specified (and encrypt=Mandatory and trustServerCertificate=false), SQL Server uses provided hostname for validating trust with the server certificate. - - - [Optional] When true, column headers are included when saving results as CSV - - - [Required] Specify the server name to connect to. Use 'hostname instance' or '<server>.database.windows.net' for Azure SQL Database. - - - auth - - - database - - - port - - - server - - - should commas be placed at the beginning of each statement in a list e.g. ', mycolumn2' instead of at the end e.g. 'mycolumn1,' - - - user - - + + + + + is required. + + + $(plug) Connect to MSSQL + + + <Default> + + + <default> + + + (0 rows affected) + + + (1 row affected) + + + ({0} rows affected) + {0} is the number of rows affected + + + + Add Azure Account + + + + Add Fabric Account + + + + Create Connection Group + + + A SQL editor must have focus before executing this command + + + A firewall rule is required to access this server. + + + A highly integrated, developer-ready transactional database that auto-scales, auto-tunes, and mirrors data to OneLake for analytics across Fabric services + + + A predefined global default value for the column or binding. + + + A query is already running for this editor session. Please cancel this query or wait for its completion. + + + Accelerate schema evolution by autogenerating ORM migrations or T-SQL change scripts + + + Accept + + + Accept the SQL Server EULA to deploy a SQL Server Docker container + + + Access denied. Please ensure you have the necessary permissions to use this tool or model. + + + Access token expired for connection {0} with uri {1} + {0} is the connection id +{1} is the uri + + + Account + + + Account not found + + + Action + + + Actual Elapsed CPU Time + + + Actual Elapsed Time + + + Actual Number of Rows For All Executions + + + Add + + + Add Account + + + Add Column + + + Add Connection + + + Add Firewall Rule + + + Add Firewall Rule to {0} + {0} is the server name + + + Add Row + + + Add Server Connection + + + Add Table + + + Add a Microsoft Entra account... + + + Add account + + + Add my client IP ({0}) + {0} is the IP address of the client + + + Add new column + + + Add new foreign key + + + Additional parameters + + + Advanced + + + Advanced Connection Settings + + + Advanced Options + + + All permissions for extensions to access your connections have been cleared. + + + Allow Null + + + Allow Nulls + + + Allow this extension to access your connections + + + Alphabetical + + + Alter + + + Always Encrypted + + + Always show in new tab + + + An active connection is required for GitHub Copilot to understand your database schema and proceed. Select "{0}" to establish a connection. + {0} is the button text (e.g., 'Connect' or 'Open SQL editor and connect') + + + An error occurred refreshing nodes. See the MSSQL output channel for more details. + + + An error occurred while copying results: {0} + {0} is the error message + + + An error occurred while processing your request. + + + An error occurred while removing Microsoft Entra account: {0} + {0} is the error message + + + An error occurred while retrieving rows: {0} + {0} is the error message + + + An error occurred while searching database objects + + + An error occurred while searching database objects: {0} + {0} is the error detail returned from the search operation + + + An error occurred: {0} + {0} is the error message + + + An unexpected error occurred with the language model. Please try again. + + + An unknown error occurred. Please try again. + + + Analytics-ready by default + + + And + + + Application Intent + + + Apply + + + Apply changes to target + + + Apply contextual suggestions for SQL syntax, relationships, and constraints + + + Approve + + + Are you sure you want to delete the container {0}? This will remove both the container and its connection from VS Code. + {0} is the container name + + + Are you sure you want to delete the selected items? + + + Are you sure you want to delete {0}? + {0} is the group name + + + Are you sure you want to delete {0}? You can delete its connections as well, or move them to the root folder. + {0} is the group name + + + Are you sure you want to disconnect? + + + Are you sure you want to remove {0}? + {0} is the node label + + + Are you sure you want to update the target? + + + Are you sure you want to {0}? + {0} is the action being confirmed + + + Are you sure? + + + Authentication + + + Authentication Library has changed, please reload Visual Studio Code. + + + Authentication Type + + + Authentication error for account '{0}' (tenant '{1}'). Resolving this requires clearing your token cache, which will sign you out of all connected accounts. + {0} is the account display name +{1} is the tenant id + + + Authentication error for account. Resolving this requires clearing your token cache, which will sign you out of all connected accounts. + + + Authentication failed due to a nonce mismatch, please close Azure Data Studio and try again. + + + Authentication failed due to a state mismatch, please close ADS and try again. + + + Auto Arrange + + + Auto Arrange Confirmation + + + Auto Arrange will automatically reposition all diagram elements based on optimal layout algorithms. Any custom positioning you've created will be lost. Do you want to proceed with auto-arranging your schema diagram? + + + Automatic tuning features like automatic index creation enabled by default. + + + Available Servers + + + Average: {0} + {0} is the average + + + Average: {0} Count: {1} Sum: {2} + {0} is the average, {1} is the count, {2} is the sum + + + Azure (China) + + + Azure (Public) + + + Azure (US Government) + + + Azure Account + + + Azure Code Grant + + + Azure Device Code + + + Azure MFA + + + Azure sign in failed. + + + Azure: Sign In + + + Azure: Sign In to Azure Cloud + + + Azure: Sign In with Device Code + + + Back + + + Back to preview + + + Batch execution time: {0} + {0} is the batch time + + + Between + + + Blanks + + + Block this extension from accessing your connections + + + Brightness + + + Browse Azure + + + Browse By + + + Browse Fabric + + + CSV + + + Calling tool: {0} with {1}. + {0} is the tool function name +{1} is the SQL tool parameters + + + Cancel + + + Cancel failed: {0} + {0} is the error message + + + Cancel schema compare failed: '{0}' + {0} is the error message returned from the cancel operation + + + Canceled + + + Canceling + + + Canceling query + + + Canceling the query failed: {0} + {0} is the error message + + + Cannot cancel query as no query is running. + + + Cannot connect due to expired tokens. Please re-authenticate and try again. + + + Cannot create foreign key + + + Cannot exclude {0}. Included dependents exist + {0} is the name of the entry + + + Cannot exclude {0}. Included dependents exist, such as {1} + {0} is the name of the entry +{1} is the name of the blocking dependency preventing exclusion. + + + Cannot include {0}. Excluded dependents exist + {0} is the name of the entry + + + Cannot include {0}. Excluded dependents exist, such as {1} + {0} is the name of the entry +{1} is the name of the blocking dependency preventing inclusion. + + + Cascade + + + Change + + + Change Database + + + Change Password + + + Change database to '{1}' for connection '{0}'? + {0} is the connection ID +{1} is the database name + + + Changed Tables + + + Changed database context to "{0}" for document "{1}" + {0} is the database name +{1} is the document name + + + Changed database context to "{0}" on server "{1}" on document "{2}". + {0} is the database name +{1} is the server name +{2} is the document name + + + Changes published successfully + + + Changes saved successfully. + + + Changing database context to "{0}" on server "{1}" on document "{2}". + {0} is the database name +{1} is the server name +{2} is the document name + + + Changing database to '{1}' for connection '{0}' + {0} is the connection ID +{1} is the database name + + + Chat command not available in this VS Code version + + + Check Constraint + + + Check Constraints + + + Checking Docker Engine Configuration + + + Checking if Docker is installed + + + Checking if Docker is installed on your machine + + + Checking if Docker is running on your machine. If not, we'll start it for you. + + + Checking if Docker is started + + + Checking if the Docker Engine is configured correctly on your machine. + + + Checking pre-requisites + + + Choose An Action + + + Choose Query History + + + Choose SQL Language + + + Choose a Microsoft Entra account + + + Choose a Microsoft Entra tenant + + + Choose a connection profile from the list below + + + Choose a database from the list below + + + Choose a hostname for the container + + + Choose a name for the SQL Server Docker Container + + + Choose a port to host the SQL Server Docker Container + + + Choose an option to provision a database + + + Choose color + + + Choose the Right Version + + + Circular reference detected: '{0}' → '{1}' creates a cycle + {0} is source table +{1} is target table + + + Clear + + + Clear All + + + Clear Recent Connections List + + + Clear Selection + + + Clear Sort + + + Clear cache and refresh token + + + Clear permissions for all extensions to access your connections + + + Clear token cache + + + Click to cancel loading summary + + + Click to connect to a database + + + Click to load summary + + + Click to sign into an Azure account + + + Close + + + Close Designer + + + Close Find + + + Close Script Pane + + + Close properties pane + + + Close the current connection + + + Cloud + + + Collapse + + + Collapse All + + + Collapse Workspace Explorer + + + Color + + + Column + + + Column '{0}' already exists + {0} is the column name + + + Column '{0}' already has a foreign key + {0} is the column name + + + Column '{0}' cannot be null because it is a primary key + {0} is the column name + + + Column '{0}' is an identity column and cannot have a cascading foreign key + {0} is the column name + + + Column '{0}' must be a primary key + {0} is the referenced column + + + Column '{0}' not found + {0} is the column name + + + Column Name + + + Column max length cannot be empty + + + Column name cannot be empty + + + Column width must be at least {0} pixels. + {0} is the minimum column width in pixels + + + Columns + + + Columns in the primary key. + + + Command Timeout + + + Commands + + + Compare + + + Compare SQL Server editions + + + Comparison Details + + + Configure Linux containers + + + Configure Rosetta in Docker Desktop + + + Configure and customize SQL Server containers + + + Confirm Password + + + Confirm SQL Server admin password + + + Confirm new password + + + Confirm to clear recent connections list + + + Confirm to remove this profile. + + + Connect + + + Connect to Database + + + Connect to MSSQL + + + Connect to Server + + + Connect to a database + + + Connect to server {0} and database {1}? + {0} is the server name +{1} is the database name + + + Connect to server {0}? + {0} is the server name + + + Connect to {0} + {0} is the name of the connection profile + + + Connect using profile {0}? + {0} is the profile ID + + + Connected successfully + + + Connected to server "{0}" on document "{1}". Server information: {2} + {0} is the server name +{1} is the document name +{2} is the server info + + + Connected to: + + + Connecting + + + Connecting to Container + + + Connecting to Database + + + Connecting to server "{0}" on document "{1}". + {0} is the server name +{1} is the document name + + + Connecting to server {0} + {0} is the server name + + + Connecting to server {0} and database {1} + {0} is the server name +{1} is the database name + + + Connecting to your SQL Server Docker container + + + Connecting to {0}... + {0} is the connection display name + + + Connecting to: + + + Connecting using profile {0} + {0} is the profile ID + + + Connecting... + + + Connection Authentication + + + Connection Details + + + Connection Dialog + + + Connection Error + + + Connection Failed + + + Connection Group + + + Connection Profile could not be updated. Please modify the connection details manually in settings.json and try again. + + + Connection String + + + Connection Timeout + + + Connection error + + + Connection is not active. Please establish a connection before performing this action. + + + Connection not found for uri "{0}". + {0} is the uri + + + Connection profile '{0}' not found. + {0} is the profile ID + + + Connection sharing permission denied for extension: '{0}'. Use the permission management commands to change this. + {0} is the extension ID + + + Connection sharing permission is required for extension: '{0}' + {0} is the extension ID + + + Connection string is required + + + Connection with ID "{0}" not found. Please verify the connection ID exists. + {0} is the connection ID + + + Consider adding a name for this foreign key + + + Container Name + + + Container creation isn't supported on this system. ARM support will be available starting with the SQL Server 2025 CU1 container image. + + + Container does not exist. Would you like to remove the connection? + + + Container failed to start within the timeout period. Please wait a few minutes and try again. + + + Contains + + + Continue Editing + + + Copied + + + Copy + + + Copy As + + + Copy Headers + + + Copy Script + + + Copy Script to Clipboard + + + Copy as CSV + + + Copy as IN clause + + + Copy as INSERT INTO + + + Copy as JSON + + + Copy code and open webpage + + + Copy connection string to clipboard + + + Copy script + + + Copy with Headers + + + Copying results... + + + Cost + + + Count: {0} + {0} is the count + + + Count: {0} Distinct Count: {1} Null Count: {2} + {0} is the count, {1} is the distinct count, and {2} is the null count + + + Create + + + Create As Script + + + Create Connection Group + + + Create Connection Profile + + + Create Container + + + Create Database + + + Create Firewall Rule + + + Create Local SQL Container + + + Create New Connection Group + + + Create a Local Docker SQL Server + + + Create a SQL Server container in seconds—no manual steps required. Manage it easily from the MSSQL extension without leaving VS Code. + + + Create a SQL database in Fabric (Preview) + + + Create a new firewall rule + + + Create new firewall rule for {0} + {0} is the server name that the firewall rule will be created for + + + Creating Container + + + Creating SQL Database for workspace {0} + {0} is the workspace ID + + + Creating and starting your SQL Server container + + + Creating workspace with capacity {0} + {0} is the capacity ID + + + Credential Error: An error occurred while attempting to refresh account credentials. Please re-authenticate. + + + Currently signed in as: + + + Custom Zoom + + + DacFx service is not available + + + Data Type + + + Data automatically replicated to OneLake in real time with a SQL analytics endpoint. + + + Data type mismatch: '{0}' in column '{1}' incompatible with '{2}' in '{3}' + {0} is source data type +{1} is source column +{2} is target data type +{3} is target column + + + Data-tier Application File (.dacpac) + + + Database + + + Database - {0} + {0} is the database name + + + Database Description + + + Database Name + + + Database Name is required + + + Database Project + + + Database changed successfully + + + Database list + + + Database name + + + Database name is required + + + Default + + + Default Value + + + Definition + + + DefinitionRequestCompleted + + + DefinitionRequested + + + Delete + + + Delete Confirmation + + + Delete Contents + + + Delete Row + + + Delete saved connection + + + Deleting Container... + + + Deny + + + Deployment Failed + + + Deployment Name + + + Deployment in progress + + + Description + + + Description for the table. + + + Details + + + Developer-friendly transactional database using the Azure SQL Database Engine. + + + Disable intellisense and syntax error checking on current document + + + Disabled + + + Discard + + + Disconnect + + + Disconnect from connection '{0}'? + {0} is the connection ID + + + Disconnected on document "{0}" + {0} is the document name + + + Disconnected successfully + + + Disconnecting from connection '{0}' + {0} is the connection ID + + + Dismiss + + + Displays the data type name for the column + + + Displays the description of the column + + + Displays the unified data type (including length, scale and precision) for the column + + + Dissatisfied + + + Distinct Count: {0} + {0} is the distinct count + + + Do you mind taking a quick feedback survey about the MSSQL Extension for VS Code? + + + Do you want to always display query results in a new tab instead of the query pane? + + + Docker failed to start within the timeout period. Please manually start Docker and try again. + + + Docker is not installed or not in PATH. Please install Docker Desktop and try again. + + + Docker requires root permissions to run. Please run Docker with sudo or add your user to the docker group using sudo usermod -aG docker $USER. Then, reboot your machine and retry. + + + Don't Show Again + + + Easily set up a local SQL Server without leaving VS Code extension. Just a few clicks to install, configure, and manage your server effortlessly! + + + Edit + + + Edit Connection Group - {0} + {0} is the connection group name + + + Edit Connection Group: {0} + {0} is the name of the connection group being edited + + + Edit Connection Profile + + + Edit Table + + + Either profileId or serverName must be provided. + + + Enable 'Trust Server Certificate' + + + Enable Experiences & Reload + + + Enable Trust Server Certificate + + + Enabled + + + Encountering a problem? Share the details with us by opening a GitHub issue so we can improve! + + + Encrypt + + + Encryption was enabled on this connection; review your SSL and certificate configuration for the target SQL Server, or enable 'Trust server certificate' in the connection dialog. + + + Encryption was enabled on this connection; review your SSL and certificate configuration for the target SQL Server, or set 'Trust server certificate' to 'true' in the settings file. Note: A self-signed certificate offers only limited protection and is not a recommended practice for production environments. Do you want to enable 'Trust server certificate' on this connection and retry? + + + End IP Address + + + Ends With + + + Enter Database Description + + + Enter Database Name + + + Enter connection group name + + + Enter container name + + + Enter description (optional) + + + Enter desired column width in pixels + + + Enter hostname + + + Enter new column width + + + Enter new password + + + Enter part of an object name to search for + + + Enter password + + + Enter port + + + Enter profile name + + + Entra token cache cleared successfully. + + + Equals + + + Error + + + Error Message: + + + Error code: + + + Error connecting to server "{0}". Details: {1} + {0} is the server name +{1} is the error message + + + Error connecting to: + + + Error creating firewall rule {0}. Check your Azure account settings and try again. Error: {1} + {0} is the rule info in format 'name (startIp - endIp)' +{1} is the error message + + + Error generating text view. Please try switching back to grid view. + + + Error loading Azure account information for tenant ID '{0}' + {0} is the tenant ID + + + Error loading Azure databases for subscription {0} ({1}). Confirm that you have permission. + {0} is the subscription name +{1} is the subscription id + + + Error loading Azure databases. + + + Error loading Azure subscriptions. + + + Error loading databases + + + Error loading designer + + + Error loading preview + + + Error loading summary + + + Error loading summary: {0} + {0} is the error message + + + Error loading workspaces + + + Error loading workspaces. Please try choosing a different account or tenant. + + + Error loading; refresh to try again + + + Error migrating connection ID {0} to new format. Please recreate this connection to use it. Error: {1} + {0} is the connection id +{1} is the error message + + + Error occurred opening content in editor. + + + Error retrieving server list: {0} + {0} is the error message + + + Error running Docker commands. Please make sure Docker is running. + + + Error signing into Azure: {0} + {0} is the error message + + + Error when refreshing token + + + Error {0}: {1} + {0} is the error number +{1} is the error message + + + Error {0}: {1} Please login as a different user and change the password using ALTER LOGIN. + {0} is the error number +{1} is the error message + + + Error: + + + Error: Login failed. Retry using different credentials? + + + Error: Unable to connect using the connection information provided. Retry profile creation? + + + Excel + + + Execute + + + Executing query... + + + Execution Plan + + + Existing Azure SQL logical server + + + Existing SQL server + + + Expand + + + Expand All + + + Expand Workspace Explorer + + + Expand properties pane + + + Explore, design, and evolve database schemas using intelligent, code-first or data-first guidance + + + Explorer + + + Export + + + Expression + + + Extremely likely + + + Fabric API error occurred ({0}): {1} + {0} is the error code +{1} is the error message + + + Fabric Account + + + Fabric Account is required + + + Fabric Workspaces + + + Fabric is not supported in the current cloud ({0}). Ensure setting '{1}' is configured correctly. + {0} is the cloud name +{1} is the setting name + + + Fabric long-running API error with error code '{0}': {1} + {0} is the error code +{1} is the error message + + + Failed + + + Failed disposing query: {0} + {0} is the error message + + + Failed to apply changes: '{0}' + {0} is the error message returned from the publish changes operation + + + Failed to change database + + + Failed to connect + + + Failed to connect to database: {0} + {0} is the database name + + + Failed to connect to server. + + + Failed to connect: {0} + {0} is the error message + + + Failed to copy script: {0} + {0} is the error message + + + Failed to create a new row: {0} + {0} is the error message + + + Failed to delete credential with id: {0}. {1} + {0} is the id +{1} is the error + + + Failed to delete {0}. + Failed to delete {0}. + + + Failed to establish connection with ID "{0}". Please check connection details and network connectivity. + {0} is the connection ID + + + Failed to fetch user tokens. + + + Failed to generate publish script: '{0}' + {0} is the error message returned from the generate script operation + + + Failed to generate script: '{0}' + {0} is the error message returned from the generate script operation + + + Failed to generate script: {0} + {0} is the error message + + + Failed to get Fabric workspaces for tenant '{0} ({1})'. + {0} is the tenant name +{1} is the tenant id + + + Failed to get Fabric workspaces for tenant '{0} ({1})': {2} + {0} is the tenant name +{1} is the tenant id +{2} is the error message + + + Failed to get authentication method, please remove and re-add the account. + + + Failed to get tenant '{0}' for account '{1}'. + {0} is the tenant id +{1} is the account name + + + Failed to list databases + + + Failed to load data: {0} + {0} is the error message + + + Failed to load publish profile + + + Failed to open scmp file: '{0}' + {0} is the error message returned from the open scmp operation + + + Failed to open script: {0} + {0} is the error message + + + Failed to pull SQL Server image. Please check your network connection and try again. + + + Failed to refresh connection ${0} with uri {1}, invalid connection result. + {0} is the connection id +{1} is the uri + + + Failed to remove row: {0} + {0} is the error message + + + Failed to revert cell: {0} + {0} is the error message + + + Failed to revert row: {0} + {0} is the error message + + + Failed to save changes: {0} + {0} is the error message + + + Failed to save publish profile + + + Failed to save results. + + + Failed to save scmp file: '{0}' + {0} is the error message returned from the save scmp operation + + + Failed to start SQL Server container. Please check the error message for more details, and then try again. + + + Failed to start query. + + + Failed to start {0}. + Failed to start {0}. + + + Failed to stop {0}. + Failed to stop {0}. + + + Failed to update cell: {0} + {0} is the error message + + + Favorites + + + Fetch rows + + + Fetching {0} script... + {0} is the script type + + + File + + + Filter + + + Filter ({0}) + {0} is the number of selected tables + + + Filter Azure subscriptions + + + Filter Options + + + Filter Settings + + + Filter by keyword + + + Filter by type + + + Filter for any field... + + + Find + + + Find Next + + + Find Node + + + Find Nodes + + + Find Previous + + + Finish + + + Finished Deployment + + + Finished query execution for document "{0}" + {0} is the document name + + + Firewall rule name + + + Firewall rule successfully added. Retry profile creation? + + + Firewall rule successfully created. + + + First Page + + + Flat + + + Folder Structure + + + For numeric data, the maximum number of decimal digits that can be stored in this database object to the right of decimal point. + + + For numeric data, the maximum number of decimal digits that can be stored in this database object. + + + Foreign Column + + + Foreign Key + + + Foreign Key {0} + {0} is the index of the foreign key + + + Foreign Keys + + + Foreign Table + + + Foreign key '{0}' already exists + {0} is the foreign key name + + + Formula + + + Found pending reconnect promise for uri {0}, failed. + {0} is the uri + + + Found pending reconnect promise for uri {0}, waiting. + {0} is the uri + + + Found {0} saved connection profile(s). + {0} is the number of connection profiles + + + General + + + General Options + + + Generate Script + + + Generate mock data and seed scripts to support testing and development environments + + + Generate script to deploy changes to target + + + Generating Report. This may take a while... + + + Generating report, this might take a while... + + + Get Connection Details + + + Get Started + + + Get connection details for connection '{0}'? + {0} is the connection ID + + + Get security-related recommendations, such as avoiding SQL injection or excessive permissions + + + Getting Docker Ready... + + + Getting Fabric database '{0}' + {0} is the database ID + + + Getting Fabric workspace '{0}' + {0} is the workspace ID + + + Getting connection details for connection '{0}' + {0} is the connection ID + + + Getting connection string for SQL Endpoint '{0}' in workspace '{1}' + {0} is the SQL endpoint ID +{1} is the workspace ID + + + Getting container ready for connections + + + Getting definition ... + + + Got invalid tool use parameters: "{0}". ({1}) + {0} is the part input +{1} is the error message + + + Greater Than + + + Greater Than or Equals + + + Grid View + + + Help + + + Hide Confirm Password + + + Hide New Password + + + Hide Script + + + Hide full error message + + + Hide password + + + Hide this panel + + + Highlight Expensive Operation + + + Hostname + + + How likely it is that you would recommend the MSSQL extension to a friend or colleague? + + + How likely it is that you would recommend {0} to a friend or colleague? + {0} is the feature name + + + How many tables are in this database? + + + Hue + + + I have read the summary and understand the potential risks. + + + I'm sorry, I can only assist with SQL-related questions. + + + Ignore Tenant + + + Image tag + + + Importance + + + In progress + + + Include + + + Include Object Types + + + Include all object types + + + Index + + + Indexes + + + Initializing comparison, this might take a while... + + + Insert + + + Install Docker + + + Instant Container Setup + + + Insufficient Workspace Permissions + + + Integrated + + + Integrated & secure + + + Invalid Firewall rule name + + + Invalid IP Address + + + Invalid SQL Server password for {0}. Password must be 8–128 characters long and meet the complexity requirements. For more information see https://docs.microsoft.com/sql/relational-databases/security/password-policy + + + Invalid column width + + + Invalid connection URI provided. + + + Invalid connection URI. Please ensure you have an active database connection. + + + Invalid connection string: {0} + + + Invalid max length '{0}' + {0} is the max length + + + Is Computed + + + Is Identity + + + Is Persisted + + + Issues ({0}) + {0} is the number of issues + + + JPEG + + + JSON + + + Keep in query pane + + + Keys for token cache could not be saved in credential store, this may cause Microsoft Entra Id access token persistence issues and connection instabilities. It's likely that SqlTools has reached credential storage limit on Windows, please clear at least 2 credentials that start with "Microsoft.SqlTools|" in Windows Credential Manager and reload. + + + Last Page + + + Learn More + + + Learn more about SQL Server 2025 features + + + Length + + + Length mismatch: Column '{0}' ({1}) incompatible with '{2}' ({3}) + {0} is source column +{1} is source length +{2} is target column +{3} is target length + + + Less Than + + + Less Than or Equals + + + Line {0} + {0} is the line number + + + List Connections + + + List Databases + + + List Functions + + + List Schemas + + + List Tables + + + List Views + + + List all connections registered with the mssql extension? + + + List databases for connection '{0}'? + {0} is the connection ID + + + List functions for connection '{0}'? + {0} is the connection ID + + + List schemas for connection '{0}'? + {0} is the connection ID + + + List tables for connection '{0}'? + {0} is the connection ID + + + List views for connection '{0}'? + {0} is the connection ID + + + Listing Fabric SQL Databases for workspace '{0}' + {0} is the workspace ID + + + Listing Fabric SQL Endpoints for workspace '{0}' + {0} is the workspace ID + + + Listing Fabric capacities for tenant '{0}' + {0} is the tenant ID + + + Listing Fabric workspaces for tenant '{0}' + {0} is the tenant ID + + + Listing databases for connection '{0}' + {0} is the connection ID + + + Listing functions for connection '{0}' + {0} is the connection ID + + + Listing role assignments for workspace '${workspaceId}' + {0} is the workspace ID + + + Listing schemas for connection '{0}' + {0} is the connection ID + + + Listing server connections + + + Listing tables for connection '{0}' + {0} is the connection ID + + + Listing views for connection '{0}' + {0} is the connection ID + + + Load + + + Load from Connection String + + + Load profile... + + + Load source, target, and options saved in an .scmp file + + + Loading + + + Loading Azure Accounts + + + Loading Fabric Accounts + + + Loading Report + + + Loading Schema Designer + + + Loading Schema Designer Model... + + + Loading Table Designer + + + Loading databases in '{0}'... + {0} is the name of the workspace + + + Loading databases in selected workspace... + + + Loading deployment + + + Loading execution plan... + + + Loading fabric provisioning... + + + Loading local containers... + + + Loading results... + + + Loading summary for {0} rows (Click to cancel) + {0} is the total number of rows + + + Loading table data... + + + Loading tenants... + + + Loading text view... + + + Loading workspaces + + + Loading workspaces... + + + Loading... + + + Local SQL Server database container + + + Local development container + + + Location + + + Location (Workspace) + + + MSSQL + + + MSSQL - Azure Auth Logs + + + MSSQL Feedback + + + Manage Connection Profiles + + + Manage relationships + + + Mandatory (Recommended) + + + Mandatory (True) + + + Max Length + + + Max row count for filtering/sorting has been exceeded. To update it, navigate to User Settings and change the setting: mssql.resultsGrid.inMemoryDataProcessingThreshold + + + Max: {0} + {0} is the max + + + Maximize + + + Maximize Panel Size + + + Maximize panel size + + + Message + + + Messages + + + Metric + + + Microsoft Account + + + Microsoft Account is required + + + Microsoft Corp + + + Microsoft Entra Account + + + Microsoft Entra Id + + + Microsoft Entra Id - Universal w/ MFA Support + + + Microsoft Entra account {0} successfully added. + {0} is the account name + + + Microsoft SQL Server License Agreement + + + Microsoft reviews your feedback to improve our products, so don't share any personal data or confidential/proprietary content. + + + Microsoft will process the feedback you submit pursuant to your organization’s instructions in order to improve your and your organization’s experience with this product. If you have any questions about the use of feedback data, please contact your tenant administrator. Processing of feedback data is governed by the Microsoft Products and Services Data Protection Addendum between your organization and Microsoft, and the feedback you submit is considered Personal Data under that addendum. + + + Microsoft will process the feedback you submit pursuant to your organization’s instructions in order to improve your and your organization’s experience with this product. If you have any questions... + + + Microsoft would like your feedback + + + Min: {0} + {0} is the min + + + Move Down + + + Move Up + + + Move to Root + + + My Data + + + NULL + + + Name + + + Name of the primary key. + + + New Azure SQL logical server (Preview) + + + New Check Constraint + + + New Column + + + New Column Mapping + + + New Deployment + + + New Foreign Key + + + New Index + + + New Microsoft Entra account could not be added. + + + New Password + + + New SQL Server local development container + + + New SQL database + + + New column mapping + + + Next + + + Next Page + + + No + + + No Action + + + No Microsoft Entra account can be found for removal. + + + No Queries Available + + + No account selected + + + No active connection + + + No active connection for database change + + + No active connection for schema view + + + No active database connection in the current editor. Please establish a connection to continue. + + + No active database connection. Please connect to a database first. + + + No active text editor found. Please open a file with an active database connection. + + + No changes detected + + + No connection credentials found + + + No connection found for connectionId: {0} + {0} is the connection ID + + + No connection information found + + + No connection profile to remove. + + + No connection was found. Please connect to a server first. + + + No data available + + + No database objects found matching '{0}' + {0} is the search term + + + No databases found in the selected workspace. + + + No databases found in workspace '{0}'. + {0} is the name of the workspace + + + No items + + + No model found. + + + No need to refresh Microsoft Entra acccount token for connection {0} with uri {1} + {0} is the connection id +{1} is the uri + + + No pending changes. Make edits to generate a script. + + + No results + + + No results for the active editor + + + No results to display + + + No saved connection profiles found. + + + No schema differences were found. + + + No script available. Make changes to the table data and generate a script first. + + + No subscriptions available. Adjust your subscription filters to try again. + + + No tenant selected + + + No tools to process. + + + No workspaces found + + + No workspaces found. Please change Fabric account or tenant to view available workspaces. + + + Non-SQL Server SQL file detected. Disable IntelliSense for such files? + + + None + + + Not Between + + + Not Contains + + + Not Ends With + + + Not Equals + + + Not Starts With + + + Not likely at all + + + Not signed in + + + Not started + + + Note: A self-signed certificate offers only limited protection and is not a recommended practice for production environments. Do you want to enable 'Trust server certificate' on this connection and retry? + + + Null Count: {0} + {0} is the null count + + + Number of Rows Read + + + OK + + + OLTP, built on Azure SQL + + + Object Explorer Filter + + + Object Type + + + Off + + + On Delete + + + On Delete Action + + + On Update + + + On Update Action + + + Open + + + Open .scmp file + + + Open Publish Script + + + Open Query + + + Open Query History + + + Open SQL editor and connect + + + Open XML + + + Open in Editor + + + Open in New Tab + + + Open in SQL Editor + + + Open in editor + + + Opening Publish Script. This may take a while... + + + Opening schema designer... + + + Operator + + + Option Description + + + Optional (False) + + + Options + + + Options have changed. Recompare to see the comparison? + + + Overall, how satisfied are you with the MSSQL extension? + + + Overall, how satisfied are you with {0}? + {0} is the feature name + + + PNG + + + Parameters + + + Parent node was not TreeNodeInfo. + + + Password + + + Password (SQL Login) + + + Password is required + + + Password must be changed for '{0}' to continue logging into '{1}' + {0} is the username +{1} is the name of the server + + + Password must be changed to continue logging into '{0}' + {0} is the name of the server + + + Passwords do not match + + + Paste + + + Paste connection string from clipboard + + + Path: {0} + {0} is the path of the node in the object explorer + + + Pick from multiple SQL Server versions, including SQL Server 2025 (Preview) with built-in AI capabilities like vector search and JSON enhancements. + + + Please Accept the SQL Server EULA + + + Please choose a unique name for the container + + + Please choose a unique name for the profile + + + Please make sure the port is a number, and choose a port that is not in use + + + Please make your password 8-128 characters long. + + + Please select a workspace where you have sufficient permissions (Contributor or higher) + + + Port + + + Port must be a number between 1 and 65535 + + + Possible Data Loss detected. Please review the changes. + + + Precision + + + Precision/scale mismatch between '{0}' and '{1}' + {0} is source column +{1} is target column + + + Preview Database Updates + + + Previous + + + Previous Page + + + Previous pending reconnect promise for uri {0} is rejected with error {1}, will attempt to reconnect if necessary. + {0} is the uri +{1} is the error + + + Previous pending reconnection for uri {0}, succeeded. + {0} is the uri + + + Previous step failed. Please check the error message and try again. + + + Primary Key + + + Primary Key Columns + + + Privacy Statement + + + Processing include or exclude all differences operation. + + + Profile Name + + + Profile created and connected + + + Profile created successfully + + + Profile removed successfully + + + Properties + + + Property + + + Provider '{0}' does not have a Microsoft resource endpoint defined. + {0} is the provider + + + Provisioning + + + Publish + + + Publish Changes + + + Publish Profile + + + Publish Project + + + Publish Settings File + + + Publish Target + + + Publish profile saved to: {0} + + + Publishing Changes + + + Pulling SQL Server Image + + + Pulling the SQL Server container image. This might take a few minutes depending on your internet connection. + + + Query Plan ({0}) + {0} is the number of query plans + + + Query executed + + + Query failed + + + Query succeeded + + + Query {0}: Query cost (relative to the script): {1}% + {0} is the query number +{1} is the query cost + + + Read more + + + Readying container for connections. + + + Readying container for connections... + + + Receive natural language explanations to help developers unfamiliar with T-SQL understand code + + + Recent + + + Recent Connections + + + Recent connections list cleared + + + Redo + + + Referenced column '{0}' not found + {0} is the column name + + + Referenced table '{0}' not found + {0} is the table name + + + Refresh + + + Refresh Credentials + + + Reload Visual Studio Code + + + Remind Me Later + + + Remove + + + Remove Sort + + + Remove recent connection + + + Remove {0} + {0} is the object type + + + Replication + + + Required + + + Reset + + + Resize + + + Resize column '{0}' + {0} is the name of the column + + + Resource Group + + + Restore + + + Restore Panel Size + + + Restore panel size + + + Restore properties pane + + + Result Set {0} + {0} is the index of the result set + + + Results + + + Results ({0}) + {0} is the number of results + + + Results copied to clipboard + + + Retry + + + Reverse Alphabetical + + + Reverse-engineer existing databases by explaining SQL schemas and relationships + + + Revert Cell + + + Revert Row + + + Rosetta is required to run SQL Server container images on Apple Silicon. Enable "Use Rosetta for x86_64/amd64 emulation on Apple Silicon" in Docker Desktop > Settings > General. + + + Row created. + + + Row removed. + + + Rows per page + + + Rule name + + + Run Query + + + Run Query History + + + Run a query in the current editor, or switch to an editor that has results. + + + Run query on connection '{0}'? Query: {1} + {0} is the connection ID +{1} is the SQL query + + + Running query is not supported when the editor is in multiple selection mode. + + + Running query on connection '{0}' + {0} is the connection ID + + + SQL Analytics Endpoint + + + SQL Container Name + + + SQL Container Version + + + SQL Database + + + SQL Login + + + SQL Plan Files + + + SQL Server 2025 is not supported on ARM architecture. Please select a different SQL Server version. + + + SQL Server 2025 is not yet supported on ARM architecture. ARM support will be available starting with the SQL Server 2025 CU1 container image. + + + SQL Server Container SA Password + + + SQL Server admin password + + + SQL Server does not support Windows containers. Please switch to Linux containers in Docker Desktop settings. + + + SQL Server port number + + + SQL Server {0} - latest + {0} is the SQL Server version + + + SQL database in Fabric (Preview) + + + SQLCMD Variables + + + SVG + + + Satisfied + + + Saturation + + + Save + + + Save .scmp file + + + Save As + + + Save As... + + + Save Changes + + + Save Connection Group + + + Save Password + + + Save Password? If 'No', password will be required each time you connect + + + Save Plan + + + Save as CSV + + + Save as Excel + + + Save as INSERT + + + Save as JSON + + + Save results command cannot be used with multiple selections. + + + Save source and target, options, and excluded elements + + + Saved Connections + + + Scaffold backend components (e.g., data-access layers) based on your current database context + + + Scale + + + Scale mismatch between '{0}' and '{1}' + {0} is source column +{1} is target column + + + Schema + + + Schema Compare + + + Schema Compare Options + + + Schema Compare failed: '{0}' + {0} is the error message returned from the compare operation + + + Schema Designer + + + Schema Designer Model is ready. Changes can now be published. + + + Schema visualization opened. + + + Schema/Object Type + + + Script + + + Script As Create + + + Script copied to clipboard + + + Script copied to clipboard. + + + Search Workspaces + + + Search connection groups + + + Search for database objects... + + + Search options... + + + Search settings... + + + Search tables... + + + Search term cannot be empty + + + Search workspaces... + + + Search... + + + See + + + Select + + + Select All + + + Select Azure account with Key Vault access for column decryption + + + Select Connection + + + Select Profile + + + Select Source + + + Select Source Schema + + + Select Target + + + Select Target Schema + + + Select a Workspace + + + Select a connection group + + + Select a tenant + + + Select a valid {0} from the dropdown + {0} is the type of the dropdown's contents, e.g 'resource group' or 'server' + + + Select a workspace to view the databases in it. + + + Select a {0} for filtering + {0} is the type of the dropdown's contents, e.g 'resource group' or 'server' + + + Select all + + + Select all options + + + Select an account + + + Select an extension to manage connection sharing permissions + + + Select an object to view its definition ({0} results) + {0} is the number of results + + + Select image + + + Select new permission for extension: '{0}' + {0} is the extension name + + + Select profile to remove + + + Select subscriptions + + + Select the SQL Server Container Image + + + Selected Microsoft Entra account removed successfully. + + + Server + + + Server - {0} + {0} is the server name + + + Server Edition + + + Server Version + + + Server connection in progress. Do you want to cancel? + + + Server could not start. This could be a permissions error or an incompatibility on your system. You can try enabling device code authentication from settings. + + + Server is required + + + Server name not set. + + + Server name or ADO.NET connection string + + + Server {0} not found. + {0} is the server name + + + Set Default + + + Set Null + + + Setting up + + + Setting up container + + + Settings + + + Severity + + + Show All + + + Show Confirm Password + + + Show MSSQL output + + + Show Menu (F3) + + + Show New Password + + + Show Schema + + + Show Script + + + Show a random table definition + + + Show full error message + + + Show password + + + Show schema for connection '{0}'? + {0} is the connection ID + + + Show table relationships + + + Showing schema for connection '{0}' + {0} is the connection ID + + + Showplan XML + + + Sign In + + + Sign in + + + Sign in to a new account + + + Sign in to your Azure subscription + + + Sign in to your Azure subscription in one of the sovereign clouds. + + + Sign in to your Azure subscription with a device code. Use this in setups where the Sign In command does not work + + + Sign into Azure + + + Sign into Azure in order to add a firewall rule. + + + Sign into Fabric + + + Signing in to Azure... + + + Simple Container Management + + + Smart performance + + + Sort + + + Sort Ascending + + + Sort Descending + + + Source + + + Source Column + + + Source Name + + + Specifies whether the column is included in the primary key for the table. + + + Specifies whether the column may have a NULL value. + + + Start IP Address + + + Start Time + + + Start, stop, and remove containers directly from the extension. + + + Started executing query at + + + Started query execution for document "{0}" + {0} is the document name + + + Started saving results to + + + Starting Container... + + + Starting Docker... + + + Starting {0}... + {0} is the container name + + + Starts With + + + Stop + + + Stopping Container... + + + Submit + + + Submit an issue + + + Subscription + + + Subtree Cost + + + Succeeded + + + Succeeded with warning + + + Successfully changed to database: {0} + {0} is the database name + + + Successfully connected to server. + + + Successfully refreshed token for connection {0} with uri {1}, {2} + {0} is the connection id +{1} is the uri +{2} is the message + + + Successfully saved results to + + + Sum: {0} + {0} is the sum + + + Summary loading canceled + + + Summary loading was canceled by user + + + Switch Direction + + + Switch Source and Target + + + Switch to Grid View + + + Switch to MSAL + + + Switch to Text View + + + Switching to Linux containers was canceled. SQL Server only supports Linux containers. + + + Table + + + Table '{0}' already exists + {0} is the table name + + + Table '{0}' not found + {0} is the table name + + + Table Explorer for '{0}' has unsaved changes. Do you want to save or discard them? + {0} is the table name + + + Table Explorer: {0} (Preview) + {0} is the table name + + + Table name + + + Table name cannot be empty + + + Take Survey + + + Target + + + Target Name + + + Target Table + + + Tenant + + + Tenant ID + + + Tenant ID is required + + + Terms & Conditions + + + Test Connection + + + Testing connection profile... + + + Text View + + + The MSSQL for VS Code extension is introducing new modern data development features! Would you like to enable them? [Learn more]({0}) + {0} is a url to learn more about the new features + + + The SQL Server 2025 RTM container image isn't compatible with ARM-based systems (including Windows ARM and Apple Silicon). + + + The behavior when a user tries to delete a row with data that is involved in a foreign key relationship. + + + The behavior when a user tries to update a row with data that is involved in a foreign key relationship. + + + The columns of the index. + + + The connection with ID '{0}' does not have the 'server' property set and is being ignored. Please set the 'server' property on this connection in order to use it. + {0} is the connection ID for the connection that has been ignored + + + The custom cloud choice is not configured. Please configure the setting `{0}`. + + + The description of the check constraint. + + + The description of the foreign key. + + + The description of the index. + + + The description of the primary key. + + + The expression defining the check constraint. + + + The extension '{0}' is requesting access to your SQL Server connections. This will allow it to execute queries and access your database. + {0} is the extension name + + + The first value must be less than the second value for the {0} operator in the {1} filter + {0} is the operator for the filter +{1} is the name of the filter + + + The first value must be set for the {0} operator in the {1} filter + {0} is the operator for the filter +{1} is the name of the filter + + + The following workspace or workspace folder connections are missing the 'id' property and are being ignored. Please manually add the 'id' property to the connection in order to use it. {0} + {0} is the list of display names for the connections that have been ignored + + + The language model did not return any output. + + + The mapping between foreign key columns and primary key columns. + + + The maximum length (in characters) that can be stored in this database object. + + + The name of the check constraint. + + + The name of the column object. + + + The name of the column. + + + The name of the foreign key. + + + The name of the index. + + + The recent connections list has been cleared but there were errors while deleting some associated credentials. View the errors in the MSSQL output channel. + + + The requested model could not be found. Please check model availability or try a different model. + + + The second value must be set for the {0} operator in the {1} filter + {0} is the operator for the filter +{1} is the name of the filter + + + The table which contains the primary or unique key column. + + + There was an error updating the project + + + This database name is already in use. Please choose a different name. + + + This message couldn't be processed. If this issue persists, please check the logs and open an issue on GitHub. + + + Timestamp + + + To compare two schemas, first select a source schema and target schema, then press compare. + + + To use this command, Open a .sql file -or- Change editor language to "SQL" -or- Select T-SQL text in the active SQL editor. + + + To use this command, you must set the language to "SQL". Confirm to change language mode. + + + Toggle Tooltips + + + Tool lookup for: {0} - {1}. + {0} is the part name +{1} is the part input + + + Total execution time: {0} + {0} is the elapsed time + + + Total rows to fetch: + + + Type + + + Unable to execute the command while the extension is initializing. Please try again later. + + + Unable to expand. Please check logs for more information. + + + Unable to open Table Explorer: No target node provided. + + + Unable to read proxy agent options to get tenants. + + + Understand and document business logic embedded in stored procedures, views, and functions + + + Undo + + + Unknown error + + + Unnamed Profile + + + Unsupported architecture for Docker: {0} + {0} is the architecture name of the machine + + + Unsupported platform for Docker: {0} + {0} is the platform name of the machine + + + Update + + + Update Database + + + Update Script + + + Updating IntelliSense... + + + Usage limits exceeded. Try again later, or consider optimizing your requests. + + + Use T-SQL intellisense and syntax error checking on current document + + + Use {0} to create a new connection. + {0} is the connect command + + + User + + + User name + + + User name (SQL Login) + + + User name is required + + + Username + + + Using {0} ({1})... + {0} is the model name +{1} is whether the model can send requests + + + Using {0} to process your request... + {0} is the model name that will be processing the request + + + Value + + + Very Dissatisfied + + + Very Satisfied + + + View More + + + View mssql for Visual Studio Code release notes? + + + Visual Studio Code must be relaunched for this setting to come into effect. Please reload Visual Studio Code. + + + Warning + + + Warnings detected. Please review the changes. + + + We can't find where Docker Desktop is located on your machine. Please manually start Docker Desktop and try again. + + + We couldn't connect using the current connection information. Would you like to retry the connection or edit the connection profile? + + + What I can do for you: + + + What can we do to improve? + + + Width cannot be 0 or negative + + + Windows Authentication + + + Works with VS Code/SSMS and uses Microsoft Entra authentication and Fabric access controls. + + + Workspace + + + Workspace is required + + + Write, optimize, and troubleshoot SQL queries with AI-recommended improvements + + + Yes + + + You are not connected to any database. + + + You must accept the license + + + You must be signed into Azure in order to browse SQL databases. + + + You must be signed into Fabric in order to browse SQL databases. + + + You must review and accept the terms to proceed + + + Your Docker Engine currently runs Windows containers. SQL Server only supports Linux containers. Would you like to switch to Linux containers? + + + Your account needs re-authentication to access {0} resources. Press Open to start the authentication process. + {0} is the resource + + + Your client IP Address '{0}' does not have access to the server '{1}' you're attempting to connect to. Would you like to create new firewall rule? + {0} is the client IP address +{1} is the server name + + + Your client IP address does not have access to the server. Add a Microsoft Entra account and create a new firewall rule to enable access. + + + Your password must contain characters from at least three of the following categories: uppercase letters, lowercase letters, numbers (0-9), and special characters (!, $, #, %, etc.). + + + Your tenant '{0} ({1})' requires you to re-authenticate again to access {2} resources. Press Open to start the authentication process. + {0} is the tenant name +{1} is the tenant id +{2} is the resource + + + Zoom In + + + Zoom Out + + + Zoom to Fit + + + [Optional] Database to connect (press Enter to connect to <default> database) + + + [Optional] Enter a display name for this connection profile + + + authenticationType + + + database + + + default + + + delete the saved connection: {0}? + {0} is the connection name + + + encrypt + + + for more details + + + hostname\instance or <server>.database.windows.net or ADO.NET connection string + + + intelliSenseUpdated + + + location + + + macOS Sierra or newer is required to use this feature. + + + resource group + + + server + + + subscription + + + test + + + untitled + + + updatingIntelliSense + + + {0} (Current Account) + {0} is the account display name + + + {0} (filtered) + + + {0} accounts + {0} is the number of accounts + + + {0} column data + {0} is the number of columns + + + {0} deleted successfully. + {0} deleted successfully. + + + {0} errors + {0} is the number of errors + + + {0} has been closed. Would you like to restore it? + {0} is the webview name + + + {0} invalid Entra accounts have been removed; you may need to run `MS SQL: Clear Microsoft Entra account token cache` and log in again. + {0} is the number of invalid accounts that have been removed + + + {0} issue + {0} is the number of issues + + + {0} issues + {0} is the number of issues + + + {0} of {1} + {0} is the number of active elements +{1} is the total number of elements + + + {0} password doesn't match the confirmation password + + + {0} properties + {0} is the object type + + + {0} rows selected, click to load summary + {0} is the number of rows to fetch summary statistics for + + + {0} selected + {0} is the number of selected rows + + + {0} started successfully. + {0} started successfully. + + + {0} stopped successfully. + {0} stopped successfully. + + + {0} warnings + {0} is the number of warnings + + + {0} {1} issue + {0} is the tab name +{1} is the number of issues + + + {0} {1} issues + {0} is the tab name +{1} is the number of issues + + + {0}. {1} + {0} is the status +{1} is the message + + + {0}: {1} + {0} is the task name +{1} is the status + + + {0}: {1}. {2} + {0} is the task name +{1} is the status +{2} is the message + + + {{put-server-name-here}} + + + ✅ Grant Access + + + ✅ Grant Access (Current) + + + ❌ Deny Access + + + ❌ Deny Access (Current) + + + 👋 I'm GitHub Copilot for MSSQL extension, your intelligent SQL development assistant in Visual Studio Code. I help you connect, explore, design, and evolve your SQL databases directly from VS Code. + + + + + Add Connection + + + Add Connection Group + + + Add Microsoft Entra Account + + + Allows users to sign in to input-constrained devices. + + + Always Encrypted + + + An execution time-out of 0 indicates an unlimited wait (no time-out) + + + Analyze Query Performance + + + Application Intent + + + Automatically adjust the column widths based on the visible rows in the result set. Could have performance problems with a large number of columns or large cells + + + Automatically display query results in a new tab instead of the query pane. This option takes effect only if `mssql.enableRichExperiences` is enabled. + + + Automatically reveal the results panel when switching to an editor with query results. Only applies when 'Open Results in Tab' is enabled. + + + Azure MFA + + + Cancel Query + + + Change Connection + + + Change Database + + + Choose SQL handler for this file + + + Chooses which Authentication method to use + + + Clear All Connection Sharing Permissions + + + Clear All Query History + + + Clear Filters + + + Clear Microsoft Entra account token cache + + + Clear Pooled Connections + + + Command Timeout + + + Connect + + + Connect to a SQL Database + + + Connection Dialog + + + Connection Timeout + + + Connection groups + + + Connection profiles defined in 'User Settings' are shown under 'MS SQL: Connect' command in the command palette. + + + Connections + + + Controls the max number of rows allowed to do filtering and sorting in memory. If the number is exceeded, sorting and filtering will be disabled. Warning: Increasing this may impact performance. + + + Copy All + + + Copy Object Name + + + Create Azure Function with SQL binding + + + Create a Table with Table Designer + + + Create a new table in your database, or edit existing tables with the table designer. Once you're done making your changes, click the 'Publish' button to send the changes to your database. + + + Default view mode for query results display. + + + Delete + + + Delete Connection Group + + + Delete Container + + + Disable Actual Plan + + + Disable Group By Schema + + + Disabled + + + Disconnect + + + Display results in a formatted text format. + + + Display results in a tabular grid format (default) + + + Do not show prompts to display query results in a new tab. + + + Do not show prompts to enable UI-based features + + + Edit Connection + + + Edit Connection Group + + + Edit Connection Sharing Permissions + + + Edit Table + + + Enable Actual Plan + + + Enable Group By Schema + + + Enable Modern Features + + + Enable Modern Features + + + Enable Parameterization for Always Encrypted + + + Enable Query History Capture + + + Enable SET ANSI_DEFAULTS + + + Enable SET ANSI_NULLS + + + Enable SET ANSI_NULL_DFLT_ON + + + Enable SET ANSI_PADDING + + + Enable SET ANSI_WARNINGS + + + Enable SET ARITHABORT option + + + Enable SET CURSOR_CLOSE_ON_COMMIT + + + Enable SET DEADLOCK_PRIORITY option + + + Enable SET IMPLICIT_TRANSACTIONS + + + Enable SET LOCK TIMEOUT option (in milliseconds) + + + Enable SET NOCOUNT option + + + Enable SET NOEXEC option + + + Enable SET PARSEONLY option + + + Enable SET QUERY_GOVERNOR_COST_LIMIT + + + Enable SET QUOTED_IDENTIFIER + + + Enable SET STATISTICS IO option + + + Enable SET STATISTICS TIME option + + + Enable SET TRANSACTION ISOLATION LEVEL option + + + Enable SET XACT_ABORT ON option + + + Enable expand/collapse buttons in Schema Designer table nodes when tables have more than 10 columns + + + Enable modern features in MSSQL + + + Enable the new set of data development features that provide a modern way to work with your SQL database in VS Code. [Enable New Experiences](command:mssql.enableRichExperiences) + + + Enabled + + + Enables UI-based features in the MSSQL extension for richer and more powerful features. Restart Visual Studio Code after changing this setting. + + + Enables connection pooling to improve overall connectivity performance. This setting is disabled by default. Visual Studio Code is required to be relaunched when the value is changed. To clear pooled connections, run the command: 'MS SQL: Clear Pooled Connections'. Note: May keep serverless databases active and prevent auto-pausing. + + + Enables experimental features in the MSSQL extension. The features are not production-ready and may have bugs or issues. Restart Visual Studio Code after changing this setting. + + + Enables use of the Sql Authentication Provider for 'Microsoft Entra Id Interactive' authentication mode when user selects 'AzureMFA' authentication. This enables Server-side resource endpoint integration when fetching access tokens. This option is only supported for 'MSAL' Authentication Library. Please restart Visual Studio Code after changing this option. + + + Estimated Plan + + + Execute Current Statement + + + Execute Query + + + Explain Query + + + Familiarize yourself with more features of the MSSQL extension that can help you be more productive. + + + Filter + + + Filter your Object Explorer Tree + + + Get Started with MSSQL for Visual Studio Code + + + Getting Started Guide + + + MSSQL Copilot + + + MSSQL configuration + + + Make a new connection to a SQL database, or edit existing connections with the connection dialog. You can connect to a database by entering your connection information, using a connection string, or browsing your Azure subscriptions. [Open Connection Dialog](command:mssql.addObjectExplorer) + + + Manage Connection Profiles + + + Maximum number of characters to store for each value in XML columns after running a query. Default value: 2,097,152. Valid value range: 1 to 2,147,483,647. + + + Maximum number of characters/bytes to store for each value in character/binary columns after running a query. Default value: 65,535. Valid value range: 1 to 2,147,483,647. + + + Maximum number of old files to remove upon startup that have expired mssql.logRetentionMinutes. Files that do not get cleaned up due to this limitation get cleaned up next time Azure Data Studio starts up. + + + Maximum number of rows to return before the server stops processing your query. + + + Maximum size of text and ntext data returned from a SELECT statement + + + New Deployment + + + New Query + + + New Query + + + New Table + + + Next Steps with MSSQL for Visual Studio Code + + + Number of minutes to retain log files for backend services. Default is 1 week. + + + Number of query history entries to show in the Query History view + + + Object Explorer filters + + + Only see the database objects that matter most to you by applying filters to the Object Explorer tree. Start by clicking the filter button next to most folders in the Connections view. + + + Open Execution Plan File + + + Open New Query and Connect + + + Open Query + + + Open Query History in Command Palette + + + Open in Copilot Agent mode + + + Open in Copilot Ask mode + + + Pause Query History Capture + + + Prevent automatic execution of scripts (e.g., 'Select Top 1000'). When enabled, scripts will not be automatically executed upon generation. + + + Prompts users to sign in using their browser. + + + Query History + + + Query Results + + + Query editor and query results pane + + + Query plan visualization + + + Refresh + + + Refresh IntelliSense Cache + + + Remove + + + Remove Microsoft Entra Account + + + Replication + + + Reveal Query Result + + + Rewrite Query + + + Run Query + + + Run a SQL Query + + + SQL Container Name + + + SQL Container Version + + + Schema Compare + + + Schema Designer + + + Script as Alter + + + Script as Create + + + Script as Drop + + + Script as Execute + + + Select Top 1000 + + + Selected Azure subscriptions for browsing and managing servers and databases + + + Send Feedback + + + Set the font family for the results grid; set to blank to use the editor font + + + Set the font size for the results grid; set to blank to use the editor size + + + Shortcuts related to the results window + + + Should BIT columns be displayed as numbers (1 or 0)? If false, BIT columns will be displayed as 'true' or 'false' + + + Should IntelliSense be enabled + + + Should IntelliSense error checking be enabled + + + Should IntelliSense quick info be enabled + + + Should IntelliSense suggestions be enabled + + + Should IntelliSense suggestions be lowercase + + + Should Personally Identifiable Information (PII) be logged in the Azure Logs output channel and the output channel log file. + + + Should Query History feature be enabled + + + Should column definitions be aligned? + + + Should data types be formatted as UPPERCASE, lowercase, or none (not formatted) + + + Should keywords be formatted as UPPERCASE, lowercase, or none (not formatted) + + + Should language service be auto-disabled when extension detects Non-MSSQL files + + + Should query result selections and scroll positions be saved when switching tabs (may impact performance) + + + Should references to objects in a select statements be split into separate lines? E.g. for 'SELECT C1, C2 FROM T1' both C1 and C2 will be on separate lines + + + Show the active SQL connection details as a CodeLens suggestion at the top of the editor for quick visibility. + + + Sort and Filter Query Results + + + Sort and filter options for query results + + + Sort and filter your query results to find the data you need quickly. + + + Sovereign cloud equivalent for `.database.fabric.microsoft.com` (including leading dot) + + + Sovereign cloud equivalent for `.database.windows.net` (including leading dot) + + + Sovereign cloud equivalent for `.datawarehouse.fabric.microsoft.com` (including leading dot) + + + Sovereign cloud equivalent for `.sql.azuresynapse.net` (including leading dot) + + + Sovereign cloud equivalent for `https://analysis.windows.net/powerbi/api/` + + + Sovereign cloud equivalent for `https://api.fabric.microsoft.com/v1/` + + + Sovereign cloud equivalent for `https://database.windows.net/` + + + Sovereign cloud equivalent for `https://vault.azure.net/` + + + Start Container + + + Start Query History Capture + + + Stop Container + + + Table Designer + + + Temporarily store passwords for connections with 'Saved Passwords' disabled, until the extension is restarted. This prevents repeated password prompts when reusing connections within the same session. + + + The additional, MSSQL-specific custom configuration for the Sovereign Cloud to use with the Microsoft Sovereign Cloud authentication provider. This along with setting `microsoft-sovereign-cloud.environment` to `custom` and providing values for `microsoft-sovereign-cloud.customEnvironment` is required to use this feature with MSSQL. + + + The color of the connection group. + + + The description of the connection group. + + + The maximum number of characters to display for the connection info in the status bar. Set to -1 for no limit. + + + The maximum number of recently used connections to store in the connection list. + + + The name of the connection group. + + + The timeout in seconds for expanding a node in Object Explorer. The default value is 45 seconds. + + + The unique identifier for the connection group this connection profile belongs to. + + + The unique identifier for the connection group. + + + The unique identifier for the parent connection group. + + + The unique identifier for this connection profile. + + + This setting will be removed in a future release. + + + Toggle SQLCMD Mode + + + True for the messages pane to be open by default; false for closed + + + Understand what your query is doing by viewing the query plan. See the estimated plan without running the query, or view the actual query plan after running the query by toggling the buttons at the top of a query editor window. + + + Use Database + + + View & Edit Data (Preview) + + + Visualize a Query Plan + + + When enabled, colorizes the connection status bar item with the color of the connection group. This setting is disabled by default. This uses the connection group folder's color directly, and does not alter it in order to ensure contrast with the current VS Code theme. Users should choose connection group colors that work well with their theme. + + + When enabled, connection groups will be collapsed instead of expanded at startup. + + + When enabled, the database objects in Object Explorer will be categorized by schema. + + + Windows Authentication + + + Write a SQL query, and run it against your database. You can also click the 'Open in New Tab' button to view your query results in their own tab, and optionally set that as the default behavior. + + + Your first steps for connecting to and developing with a SQL database + + + [Optional] Character used for enclosing text fields when saving results as CSV + + + [Optional] Character(s) used for separating rows when saving results as CSV + + + [Optional] Configuration options for copying multi-line results from the Results View + + + [Optional] Configuration options for copying results from the Results View + + + [Optional] Configuration options for which column new result panes should open in + + + [Optional] Declares the application workload type when connecting to SQL Server such as ReadWrite or ReadOnly. Refer to SQL Server AlwaysOn for more detail. + + + [Optional] Delimiter for separating data items when saving results as CSV. Choose from common separators like comma (,), tab (\t), semicolon (;), or pipe (|) + + + [Optional] Do not show unsupported platform warnings + + + [Optional] File encoding used when saving results as CSV. Choose from UTF-8 (recommended), UTF-16, ASCII, or Latin-1 based on your target application compatibility + + + [Optional] Indicates the SQL Server language settings. + + + [Optional] Indicates the name of local docker container the connection is on + + + [Optional] Indicates whether this profile has an empty password explicitly set + + + [Optional] Indicates which server type the provider will expose through the DataReader. + + + [Optional] Log debug output to the VS Code console (Help -> Toggle Developer Tools) + + + [Optional] Log level for backend services. Azure Data Studio generates a file name every time it starts and if the file already exists the logs entries are appended to that file. For cleanup of old log files see logRetentionMinutes and logFilesRemovalLimit settings. The default tracingLevel does not log much. Changing verbosity could lead to extensive logging and disk space requirements for the logs. Error includes Critical, Warning includes Error, Information includes Warning and Verbose includes Information + + + [Optional] Should execution time be shown for individual batches + + + [Optional] Specify a custom name for this connection profile to easily browse and search in the command palette of Visual Studio Code. + + + [Optional] Specify the SQL Server authentication type. + + + [Optional] Specify the database name to connect to. If database is not specified, the default user database setting is used, typically 'master'. + + + [Optional] Specify the delay between attempts to restore connection. + + + [Optional] Specify the length of time in seconds to wait for a command to execute before terminating the attempt and generating an error. The default value is 30 seconds. + + + [Optional] Specify the length of time in seconds to wait for a connection to the server before terminating connection attempt and generating an error. The default value is 30 seconds. + + + [Optional] Specify the maximum number of connections allowed in the pool. + + + [Optional] Specify the minimum amount of time in seconds for this connection to live in the pool before being removed/deleted. + + + [Optional] Specify the minimum number of connections allowed in the pool. + + + [Optional] Specify the name of the application used for SQL Server to log (default: 'vscode-mssql'). + + + [Optional] Specify the name of the primary file, including the full path name, of an attachable database. + + + [Optional] Specify the name of the workstation connecting to SQL Server. + + + [Optional] Specify the name or network address of the instance of SQL Server that acts as a failover partner. + + + [Optional] Specify the number of attempts to restore connection. + + + [Optional] Specify the password for SQL Server authentication. If password is not specified or already saved, when you connect, you will be asked again. + + + [Optional] Specify the port number to connect to. + + + [Optional] Specify the size in bytes of the network packets to communicate with SQL Server. + + + [Optional] Specify the user name for SQL Server authentication. If user name is not specified, when you connect, you will be asked again. + + + [Optional] The ADO.NET connection string to use for the connection. Overrides any other options given in this connection. + + + [Optional] Used by SQL Server in replication. + + + [Optional] When 'Mandatory' or 'Strict', SQL Server uses SSL encryption for all data sent between the client and server if the server has a certificate installed. When set to 'Strict', SQL Server uses TDS 8.0 for all data transfer between the client and server. 'Strict' is supported on SQL Server 2022 onwards. + + + [Optional] When set to 'true', multiple result sets can be returned and read from on connection. + + + [Optional] When set to 'true', the SQL Server SSL certificate is automatically trusted when the communication layer is encrypted using SSL. Set 'false' for Azure SQL Database connection. + + + [Optional] When set to 'true', the connection object is drawn from the appropriate pool, or if necessary, is created and added to the appropriate pool. Note: May keep serverless databases active and prevent auto-pausing. + + + [Optional] When set to 'true', the detection and connection to the active server is faster if AlwaysOn Availability Group is configured on different subnets. + + + [Optional] When set to 'true', the password for SQL Server authentication is saved in the secure store of your operating system such as KeyChain in MacOS or Secure Store in Windows. + + + [Optional] When set to false, security-sensitive information, such as the password, is not returned as part of the connection if the connection is open or has ever been in an open state. + + + [Optional] When specified (and encrypt=Mandatory and trustServerCertificate=false), SQL Server uses provided hostname for validating trust with the server certificate. + + + [Optional] When true, column headers are included when saving results as CSV + + + [Required] Specify the server name to connect to. Use 'hostname instance' or '<server>.database.windows.net' for Azure SQL Database. + + + auth + + + database + + + port + + + server + + + should commas be placed at the beginning of each statement in a list e.g. ', mycolumn2' instead of at the end e.g. 'mycolumn1,' + + + user + + \ No newline at end of file diff --git a/media/objectTypes/EditTableData_Dark.svg b/media/objectTypes/EditTableData_Dark.svg new file mode 100644 index 0000000000..f2e021a51b --- /dev/null +++ b/media/objectTypes/EditTableData_Dark.svg @@ -0,0 +1,5 @@ + + + diff --git a/media/objectTypes/EditTableData_Light.svg b/media/objectTypes/EditTableData_Light.svg new file mode 100644 index 0000000000..e0e531fb9e --- /dev/null +++ b/media/objectTypes/EditTableData_Light.svg @@ -0,0 +1,4 @@ + + + diff --git a/package.json b/package.json index 40c3431ac0..64c17088bf 100644 --- a/package.json +++ b/package.json @@ -178,6 +178,7 @@ "pretty-data": "^0.40.0", "semver": "https://registry.npmjs.org/semver/-/semver-5.0.3.tgz", "shallow-equal": "^3.1.0", + "slickgrid-react": "^9.8.0", "tar": "^7.4.3", "tmp": "^0.2.4", "tunnel": "0.0.6", @@ -583,6 +584,11 @@ "when": "view == objectExplorer && viewItem =~ /\\btype=(Table)\\b/", "group": "4_MSSQL_object@1" }, + { + "command": "mssql.tableExplorer", + "when": "view == objectExplorer && viewItem =~ /\\btype=(Table)\\b/ && config.mssql.enableRichExperiences", + "group": "4_MSSQL_object@2" + }, { "command": "mssql.filterNode", "when": "view == objectExplorer && viewItem =~ /\\bfilterable=true\\b.*\\bhasFilters=false\\b/", @@ -905,6 +911,11 @@ "title": "%mssql.schemaCompare%", "category": "MS SQL" }, + { + "command": "mssql.tableExplorer", + "title": "%mssql.tableExplorer%", + "category": "MS SQL" + }, { "command": "mssql.rebuildIntelliSenseCache", "title": "%mssql.rebuildIntelliSenseCache%", diff --git a/package.nls.json b/package.nls.json index 5dd0753145..69494304be 100644 --- a/package.nls.json +++ b/package.nls.json @@ -45,6 +45,7 @@ "mssql.copilot.rewriteQuery": "Rewrite Query", "mssql.copilot.newQueryWithConnection": "Open New Query and Connect", "mssql.schemaCompare": "Schema Compare", + "mssql.tableExplorer": "View & Edit Data (Preview)", "mssql.toggleSqlCmd": "Toggle SQLCMD Mode", "mssql.copyObjectName": "Copy Object Name", "mssql.addAadAccount": "Add Microsoft Entra Account", diff --git a/scripts/bundle-reactviews.js b/scripts/bundle-reactviews.js index 5cd89caa90..ce16522bbd 100644 --- a/scripts/bundle-reactviews.js +++ b/scripts/bundle-reactviews.js @@ -27,6 +27,7 @@ const config = { schemaCompare: "src/reactviews/pages/SchemaCompare/index.tsx", changePassword: "src/reactviews/pages/ChangePassword/index.tsx", publishProject: "src/reactviews/pages/PublishProject/index.tsx", + tableExplorer: "src/reactviews/pages/TableExplorer/index.tsx", }, bundle: true, outdir: "dist/views", diff --git a/src/constants/constants.ts b/src/constants/constants.ts index 3b503af78c..2a956c9be0 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -51,6 +51,8 @@ export const cmdCommandPaletteQueryHistory = "mssql.commandPaletteQueryHistory"; export const cmdNewQuery = "mssql.newQuery"; export const cmdCopilotNewQueryWithConnection = "mssql.copilot.newQueryWithConnection"; export const cmdSchemaCompare = "mssql.schemaCompare"; +export const cmdTableExplorer = "mssql.tableExplorer"; +export const cmdTableNodeAction = "mssql.tableNodeAction"; export const cmdSchemaCompareOpenFromCommandPalette = "mssql.schemaCompareOpenFromCommandPalette"; export const cmdPublishDatabaseProject = "mssql.publishDatabaseProject"; export const cmdManageConnectionProfiles = "mssql.manageProfiles"; diff --git a/src/constants/locConstants.ts b/src/constants/locConstants.ts index bcbb8b1a84..4ebd535786 100644 --- a/src/constants/locConstants.ts +++ b/src/constants/locConstants.ts @@ -2075,3 +2075,111 @@ export class ConnectionGroup { }); }; } + +export class TableExplorer { + public static unableToOpenTableExplorer = l10n.t( + "Unable to open Table Explorer: No target node provided.", + ); + public static changesSavedSuccessfully = l10n.t("Changes saved successfully."); + public static rowCreatedSuccessfully = l10n.t("Row created."); + public static rowRemoved = l10n.t("Row removed."); + + public static title = (tableName: string) => + l10n.t({ + message: "Table Explorer: {0} (Preview)", + args: [tableName], + comment: ["{0} is the table name"], + }); + + public static failedToSaveChanges = (errorMessage: string) => + l10n.t({ + message: "Failed to save changes: {0}", + args: [errorMessage], + comment: ["{0} is the error message"], + }); + + public static failedToLoadData = (errorMessage: string) => + l10n.t({ + message: "Failed to load data: {0}", + args: [errorMessage], + comment: ["{0} is the error message"], + }); + + public static failedToCreateNewRow = (errorMessage: string) => + l10n.t({ + message: "Failed to create a new row: {0}", + args: [errorMessage], + comment: ["{0} is the error message"], + }); + + public static failedToRemoveRow = (errorMessage: string) => + l10n.t({ + message: "Failed to remove row: {0}", + args: [errorMessage], + comment: ["{0} is the error message"], + }); + + public static failedToUpdateCell = (errorMessage: string) => + l10n.t({ + message: "Failed to update cell: {0}", + args: [errorMessage], + comment: ["{0} is the error message"], + }); + + public static failedToRevertCell = (errorMessage: string) => + l10n.t({ + message: "Failed to revert cell: {0}", + args: [errorMessage], + comment: ["{0} is the error message"], + }); + + public static failedToRevertRow = (errorMessage: string) => + l10n.t({ + message: "Failed to revert row: {0}", + args: [errorMessage], + comment: ["{0} is the error message"], + }); + + public static failedToGenerateScript = (errorMessage: string) => + l10n.t({ + message: "Failed to generate script: {0}", + args: [errorMessage], + comment: ["{0} is the error message"], + }); + + public static noScriptToOpen = l10n.t( + "No script available. Make changes to the table data and generate a script first.", + ); + + public static failedToOpenScript = (errorMessage: string) => + l10n.t({ + message: "Failed to open script: {0}", + args: [errorMessage], + comment: ["{0} is the error message"], + }); + + public static scriptCopiedToClipboard = l10n.t("Script copied to clipboard."); + + public static noScriptToCopy = l10n.t( + "No script available. Make changes to the table data and generate a script first.", + ); + + public static failedToCopyScript = (errorMessage: string) => + l10n.t({ + message: "Failed to copy script: {0}", + args: [errorMessage], + comment: ["{0} is the error message"], + }); + + public static unsavedChangesPrompt = (tableName: string) => + l10n.t({ + message: + "Table Explorer for '{0}' has unsaved changes. Do you want to save or discard them?", + args: [tableName], + comment: ["{0} is the table name"], + }); + + public static Save = l10n.t("Save"); + public static Discard = l10n.t("Discard"); + public static Cancel = l10n.t("Cancel"); +} diff --git a/src/controllers/mainController.ts b/src/controllers/mainController.ts index 8e7649bc04..78711dd008 100644 --- a/src/controllers/mainController.ts +++ b/src/controllers/mainController.ts @@ -93,6 +93,8 @@ import { import { ScriptOperation } from "../models/contracts/scripting/scriptingRequest"; import { getCloudId } from "../azure/providerSettings"; import { openExecutionPlanWebview } from "./sharedExecutionPlanUtils"; +import { ITableExplorerService, TableExplorerService } from "../services/tableExplorerService"; +import { TableExplorerWebViewController } from "../tableExplorer/tableExplorerWebViewController"; /** * The main controller class that initializes the extension @@ -115,6 +117,7 @@ export default class MainController implements vscode.Disposable { public sqlTasksService: SqlTasksService; public dacFxService: DacFxService; public schemaCompareService: SchemaCompareService; + public tableExplorerService: ITableExplorerService; public sqlProjectsService: SqlProjectsService; public azureAccountService: AzureAccountService; public azureResourceService: AzureResourceService; @@ -552,6 +555,7 @@ export default class MainController implements vscode.Disposable { this.dacFxService = new DacFxService(SqlToolsServerClient.instance); this.sqlProjectsService = new SqlProjectsService(SqlToolsServerClient.instance); this.schemaCompareService = new SchemaCompareService(SqlToolsServerClient.instance); + this.tableExplorerService = new TableExplorerService(SqlToolsServerClient.instance); const azureResourceController = new AzureResourceController(); this.azureAccountService = new AzureAccountService( this._connectionMgr.azureController, @@ -1386,6 +1390,33 @@ export default class MainController implements vscode.Disposable { }); this._context.subscriptions.push(this.objectExplorerTree); + // Register command for table node double-click action + let lastTableClickTime = 0; + let lastTableNode: TreeNodeInfo | undefined; + const doubleClickThreshold = 500; // milliseconds + + this._context.subscriptions.push( + vscode.commands.registerCommand(Constants.cmdTableNodeAction, (node: TreeNodeInfo) => { + const currentTime = Date.now(); + + // Check if this is a double-click on the same node + if ( + lastTableNode === node && + currentTime - lastTableClickTime < doubleClickThreshold + ) { + // Double-click detected - open Table Explorer + void this.onTableExplorer(node); + // Reset to prevent triple-click + lastTableNode = undefined; + lastTableClickTime = 0; + } else { + // Single click - just track it + lastTableNode = node; + lastTableClickTime = currentTime; + } + }), + ); + // Old style Add connection when experimental features are not enabled // Add Object Explorer Node @@ -1612,6 +1643,12 @@ export default class MainController implements vscode.Disposable { ), ); + this._context.subscriptions.push( + vscode.commands.registerCommand(Constants.cmdTableExplorer, async (node: any) => + this.onTableExplorer(node), + ), + ); + this._context.subscriptions.push( vscode.commands.registerCommand( Constants.cmdSchemaCompareOpenFromCommandPalette, @@ -2677,6 +2714,18 @@ export default class MainController implements vscode.Disposable { schemaCompareWebView.revealToForeground(); } + public async onTableExplorer(node?: any): Promise { + const tableExplorerWebView = new TableExplorerWebViewController( + this._context, + this._vscodeWrapper, + this.tableExplorerService, + this._connectionMgr, + node, + ); + + tableExplorerWebView.revealToForeground(); + } + /** * Handler for the Publish Database Project command. * Accepts the project file path as an argument. diff --git a/src/controllers/reactWebviewPanelController.ts b/src/controllers/reactWebviewPanelController.ts index 5080b06cde..73c3d6d2ca 100644 --- a/src/controllers/reactWebviewPanelController.ts +++ b/src/controllers/reactWebviewPanelController.ts @@ -104,7 +104,7 @@ export class ReactWebviewPanelController< this._panel.reveal(viewColumn, true); } - private async showRestorePrompt(): Promise<{ + protected async showRestorePrompt(): Promise<{ title: string; run: () => Promise; }> { diff --git a/src/models/contracts/tableExplorer.ts b/src/models/contracts/tableExplorer.ts new file mode 100644 index 0000000000..9200ed6cbf --- /dev/null +++ b/src/models/contracts/tableExplorer.ts @@ -0,0 +1,146 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as tableExplorer from "../../sharedInterfaces/tableExplorer"; +import { RequestType, NotificationType } from "vscode-languageclient"; + +//#region edit/initialize + +export namespace EditInitializeRequest { + export const type = new RequestType< + tableExplorer.EditInitializeParams, + tableExplorer.EditInitializeResult, + void, + void + >("edit/initialize"); +} + +//#endregion + +//#region edit/sessionReady + +export namespace EditSessionReadyNotification { + export const type = new NotificationType( + "edit/sessionReady", + ); +} + +//#endregion + +//#region edit/subset + +export namespace EditSubsetRequest { + export const type = new RequestType< + tableExplorer.EditSubsetParams, + tableExplorer.EditSubsetResult, + void, + void + >("edit/subset"); +} + +//#endregion + +//#region edit/commit +export namespace EditCommitRequest { + export const type = new RequestType< + tableExplorer.EditCommitParams, + tableExplorer.EditCommitResult, + void, + void + >("edit/commit"); +} + +//#endregion + +//#region edit/createRow + +export namespace EditCreateRowRequest { + export const type = new RequestType< + tableExplorer.EditCreateRowParams, + tableExplorer.EditCreateRowResult, + void, + void + >("edit/createRow"); +} + +//#endregion + +//#region edit/deleteRow + +export namespace EditDeleteRowRequest { + export const type = new RequestType< + tableExplorer.EditDeleteRowParams, + tableExplorer.EditDeleteRowResult, + void, + void + >("edit/deleteRow"); +} + +//#endregion + +//#region edit/revertRow + +export namespace EditRevertRowRequest { + export const type = new RequestType< + tableExplorer.EditRevertRowParams, + tableExplorer.EditRevertRowResult, + void, + void + >("edit/revertRow"); +} + +//#endregion + +//#region edit/updateCell + +export namespace EditUpdateCellRequest { + export const type = new RequestType< + tableExplorer.EditUpdateCellParams, + tableExplorer.EditUpdateCellResult, + void, + void + >("edit/updateCell"); +} + +//#endregion + +//#region edit/revertCell + +export namespace EditRevertCellRequest { + export const type = new RequestType< + tableExplorer.EditRevertCellParams, + tableExplorer.EditRevertCellResult, + void, + void + >("edit/revertCell"); +} + +//#endregion + +//#region edit/dispose + +export namespace EditDisposeRequest { + export const type = new RequestType< + tableExplorer.EditDisposeParams, + tableExplorer.EditDisposeResult, + void, + void + >("edit/dispose"); +} + +//#endregion + +//#region edit/script + +export namespace EditScriptRequest { + export const type = new RequestType< + tableExplorer.EditScriptParams, + tableExplorer.EditScriptResult, + void, + void + >("edit/script"); +} + +//#endregion diff --git a/src/objectExplorer/nodes/treeNodeInfo.ts b/src/objectExplorer/nodes/treeNodeInfo.ts index 7d259a6e80..369239a7da 100644 --- a/src/objectExplorer/nodes/treeNodeInfo.ts +++ b/src/objectExplorer/nodes/treeNodeInfo.ts @@ -69,6 +69,20 @@ export class TreeNodeInfo extends vscode.TreeItem implements ITreeNodeInfo { this.iconPath = ObjectExplorerUtils.iconPath(this.nodeType); } this.id = this.generateId(); + + // Add command for table nodes to handle double-click + if (this._nodeType === "Table") { + const config = vscode.workspace.getConfiguration("mssql"); + const enableRichExperiences = config.get("enableRichExperiences", true); + + if (enableRichExperiences) { + this.command = { + command: Constants.cmdTableNodeAction, + title: "", + arguments: [this], + }; + } + } } // Generating a unique ID for the node diff --git a/src/reactviews/common/locConstants.ts b/src/reactviews/common/locConstants.ts index c87b9d67f5..ca5c3ba57a 100644 --- a/src/reactviews/common/locConstants.ts +++ b/src/reactviews/common/locConstants.ts @@ -1086,6 +1086,37 @@ export class LocConstants { passwordsDoNotMatch: l10n.t("Passwords do not match"), }; } + + public get tableExplorer() { + return { + saveChanges: l10n.t("Save Changes"), + addRow: l10n.t("Add Row"), + showScript: l10n.t("Show Script"), + hideScript: l10n.t("Hide Script"), + openInEditor: l10n.t("Open in Editor"), + openInSqlEditor: l10n.t("Open in SQL Editor"), + copyScript: l10n.t("Copy Script"), + copyScriptToClipboard: l10n.t("Copy Script to Clipboard"), + maximizePanelSize: l10n.t("Maximize Panel Size"), + restorePanelSize: l10n.t("Restore Panel Size"), + updateScript: l10n.t("Update Script"), + commands: l10n.t("Commands"), + deleteRow: l10n.t("Delete Row"), + revertCell: l10n.t("Revert Cell"), + revertRow: l10n.t("Revert Row"), + totalRowsToFetch: l10n.t("Total rows to fetch:"), + rowsPerPage: l10n.t("Rows per page"), + fetchRows: l10n.t("Fetch rows"), + firstPage: l10n.t("First Page"), + previousPage: l10n.t("Previous Page"), + nextPage: l10n.t("Next Page"), + lastPage: l10n.t("Last Page"), + loadingTableData: l10n.t("Loading table data..."), + noDataAvailable: l10n.t("No data available"), + noPendingChanges: l10n.t("No pending changes. Make edits to generate a script."), + closeScriptPane: l10n.t("Close Script Pane"), + }; + } } export let locConstants = LocConstants.getInstance(); diff --git a/src/reactviews/pages/TableExplorer/TableDataGrid.css b/src/reactviews/pages/TableExplorer/TableDataGrid.css new file mode 100644 index 0000000000..48c95a6ab9 --- /dev/null +++ b/src/reactviews/pages/TableExplorer/TableDataGrid.css @@ -0,0 +1,213 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* Container styles */ +.table-explorer-grid-container { + width: 100%; + max-width: 100%; + height: 100%; + display: flex; + flex-direction: column; +} + +/* Grid base styles */ +#tableExplorerGrid { + --slick-border-color: var(--vscode-editorWidget-border); + --slick-cell-border-right: 1px solid var(--vscode-editorWidget-border); + --slick-cell-border-top: 1px solid var(--vscode-editorWidget-border); + --slick-cell-border-bottom: 0; + --slick-cell-border-left: 0; + --slick-cell-box-shadow: none; + --slick-grid-border-color: var(--vscode-editorWidget-border); + width: 100%; + max-width: 100%; + flex: 1; + min-height: 0; +} + +#tableExplorerGrid .slick-viewport { + overflow-x: hidden !important; +} + +#tableExplorerGrid .slick-cell { + display: flex; + align-items: center; + line-height: 1.2; /* Tighter line height for more compact rows */ + padding: 2px 4px; /* Reduced padding for more compact cells */ +} + +#tableExplorerGrid .slick-row { + height: 26px; /* Explicit row height to match the rowHeight option */ +} + +/* Force theme colors on SlickGrid elements */ +#tableExplorerGrid .slick-header, +#tableExplorerGrid .slick-headerrow, +#tableExplorerGrid .slick-footerrow, +#tableExplorerGrid .slick-top-panel { + background-color: var(--vscode-editor-background) !important; + color: var(--vscode-foreground) !important; + border-bottom: 1px solid var(--vscode-panel-border) !important; + height: 26px !important; /* Match data row height for uniform appearance */ +} + +#tableExplorerGrid .slick-header-column { + background-color: var(--vscode-editor-background) !important; + color: var(--vscode-foreground) !important; + border-right: 1px solid var(--vscode-panel-border) !important; + border-bottom: 1px solid var(--vscode-panel-border) !important; + text-align: center !important; + font-weight: 600 !important; + font-size: 12px !important; + padding: 4px 8px !important; /* Adjusted padding for 26px height */ + line-height: 1.2 !important; + height: 26px !important; /* Explicit height to match data rows */ + box-sizing: border-box !important; /* Ensure padding is included in height */ + display: flex !important; + align-items: center !important; + justify-content: center !important; +} + +#tableExplorerGrid .slick-header-column:last-child { + border-right: none !important; +} + +#tableExplorerGrid .slick-header-column .slick-column-name { + text-align: center !important; + width: 100% !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + height: 100% !important; + margin: 0 !important; /* Remove excessive margin that pushes text out of view */ +} + +#tableExplorerGrid .slick-cell, +#tableExplorerGrid .slick-row { + background-color: var(--vscode-editor-background) !important; + color: var(--vscode-foreground) !important; +} + +#tableExplorerGrid .slick-row.odd { + background-color: var(--vscode-editor-background) !important; +} + +#tableExplorerGrid .slick-row.even { + background-color: var(--vscode-editor-background) !important; +} + +#tableExplorerGrid .slick-row:hover { + background-color: var(--vscode-list-hoverBackground) !important; +} + +#tableExplorerGrid .slick-row.selected { + background-color: var(--vscode-list-activeSelectionBackground) !important; + color: var(--vscode-list-activeSelectionForeground) !important; +} + +/* Fix viewport canvas background */ +#tableExplorerGrid + > div.slick-pane.slick-pane-top.slick-pane-left + > div.slick-viewport.slick-viewport-top.slick-viewport-left + > div { + background-color: var(--vscode-editor-background) !important; +} + +/* VS Code-style context menu */ +.slick-context-menu { + background-color: var(--vscode-menu-background) !important; + border: 1px solid var(--vscode-menu-border) !important; + border-radius: 5px !important; + box-shadow: 0 2px 8px var(--vscode-widget-shadow) !important; + padding: 4px 0 !important; + min-width: 180px !important; +} + +.slick-context-menu .slick-menu-item { + background-color: transparent !important; + color: var(--vscode-menu-foreground) !important; + padding: 4px 20px 4px 30px !important; + line-height: 22px !important; + font-size: 13px !important; + border: none !important; + cursor: pointer !important; + position: relative !important; + display: flex !important; + align-items: center !important; + white-space: nowrap !important; +} + +.slick-context-menu .slick-menu-item:hover { + background-color: var(--vscode-menu-selectionBackground) !important; + color: var(--vscode-menu-selectionForeground) !important; +} + +.slick-context-menu .slick-menu-item .slick-menu-icon { + position: absolute !important; + left: 8px !important; + width: 16px !important; + height: 16px !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; +} + +.slick-context-menu .slick-menu-item .slick-menu-content { + flex: 1 !important; + padding: 0 !important; + margin: 0 !important; +} + +.slick-context-menu .slick-menu-item.red { + color: var(--vscode-menu-foreground) !important; +} + +.slick-context-menu .slick-menu-item.red:hover { + color: var(--vscode-menu-selectionForeground) !important; +} + +.slick-context-menu .slick-menu-item .mdi { + color: var(--vscode-menu-foreground) !important; + font-size: 16px !important; +} + +.slick-context-menu .slick-menu-item:hover .mdi { + color: var(--vscode-menu-selectionForeground) !important; +} + +.slick-context-menu .slick-menu-item.bold, +.slick-context-menu .slick-menu-item .bold { + font-weight: normal !important; +} + +/* Cell state styles */ +.table-cell-error { + background-color: var(--vscode-inputValidation-errorBackground, #5a1d1d); + padding: 1px 4px; /* Reduced top/bottom padding for compact rows */ + height: 100%; + width: 100%; + box-sizing: border-box; + line-height: 1.2; +} + +.table-cell-modified { + background-color: var(--vscode-inputValidation-warningBackground, #fffbe6); + padding: 1px 4px; /* Reduced top/bottom padding for compact rows */ + height: 100%; + width: 100%; + box-sizing: border-box; + line-height: 1.2; +} + +.table-cell-null { + font-style: italic; + color: var(--vscode-editorGhostText-foreground, #888); +} + +/* Row number column */ +.table-row-number { + color: var(--vscode-foreground); + padding-left: 8px; +} diff --git a/src/reactviews/pages/TableExplorer/TableDataGrid.tsx b/src/reactviews/pages/TableExplorer/TableDataGrid.tsx new file mode 100644 index 0000000000..3d6803dc6b --- /dev/null +++ b/src/reactviews/pages/TableExplorer/TableDataGrid.tsx @@ -0,0 +1,599 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React, { + useState, + useEffect, + useRef, + useImperativeHandle, + forwardRef, + useMemo, +} from "react"; +import { + SlickgridReactInstance, + Column, + GridOption, + SlickgridReact, + Editors, + ContextMenu, +} from "slickgrid-react"; +import { EditSubsetResult } from "../../../sharedInterfaces/tableExplorer"; +import { ColorThemeKind } from "../../../sharedInterfaces/webview"; +import { locConstants as loc } from "../../common/locConstants"; +import TableExplorerCustomPager from "./TableExplorerCustomPager"; +import "@slickgrid-universal/common/dist/styles/css/slickgrid-theme-default.css"; +import "./TableDataGrid.css"; + +interface TableDataGridProps { + resultSet: EditSubsetResult | undefined; + themeKind?: ColorThemeKind; + pageSize?: number; + currentRowCount?: number; + failedCells?: string[]; + onDeleteRow?: (rowId: number) => void; + onUpdateCell?: (rowId: number, columnId: number, newValue: string) => void; + onRevertCell?: (rowId: number, columnId: number) => void; + onRevertRow?: (rowId: number) => void; + onLoadSubset?: (rowCount: number) => void; + onCellChangeCountChanged?: (count: number) => void; +} + +export interface TableDataGridRef { + clearAllChangeTracking: () => void; + getCellChangeCount: () => number; + goToLastPage: () => void; + goToFirstPage: () => void; +} + +export const TableDataGrid = forwardRef( + ( + { + resultSet, + themeKind, + pageSize = 100, + currentRowCount, + failedCells, + onDeleteRow, + onUpdateCell, + onRevertCell, + onRevertRow, + onLoadSubset, + onCellChangeCountChanged, + }, + ref, + ) => { + const [columns, setColumns] = useState([]); + const [options, setOptions] = useState(undefined); + const [dataset, setDataset] = useState([]); + const [currentTheme, setCurrentTheme] = useState(themeKind); + const reactGridRef = useRef(null); + const cellChangesRef = useRef>(new Map()); + const failedCellsRef = useRef>(new Set()); + const lastPageRef = useRef(1); + const lastItemsPerPageRef = useRef(pageSize); + const previousResultSetRef = useRef(undefined); + const isInitializedRef = useRef(false); + + // Create a custom pager component with bound props + const BoundCustomPager = useMemo( + () => + React.forwardRef((pagerProps, pagerRef) => ( + + )), + [currentRowCount, onLoadSubset], + ); + + function reactGridReady(reactGrid: SlickgridReactInstance) { + reactGridRef.current = reactGrid; + isInitializedRef.current = true; + } + + // Clear all change tracking (called after successful save) + function clearAllChangeTracking() { + cellChangesRef.current.clear(); + failedCellsRef.current.clear(); + // Force grid to re-render to remove all colored backgrounds + if (reactGridRef.current?.slickGrid) { + reactGridRef.current.slickGrid.invalidate(); + } + + // Notify parent of change count update + if (onCellChangeCountChanged) { + onCellChangeCountChanged(0); + } + } + + // Expose methods to parent via ref + useImperativeHandle(ref, () => ({ + clearAllChangeTracking, + getCellChangeCount: () => cellChangesRef.current.size, + goToLastPage: () => { + if (reactGridRef.current?.paginationService && reactGridRef.current?.dataView) { + const totalItems = reactGridRef.current.dataView.getLength(); + const itemsPerPage = reactGridRef.current.paginationService.itemsPerPage; + const lastPage = Math.ceil(totalItems / itemsPerPage); + void reactGridRef.current.paginationService.goToPageNumber(lastPage); + } + }, + goToFirstPage: () => { + if (reactGridRef.current?.paginationService) { + void reactGridRef.current.paginationService.goToPageNumber(1); + } + }, + })); // Convert a single row to grid format + function convertRowToDataRow(row: any): any { + const dataRow: any = { + id: row.id, + }; + row.cells.forEach((cell: any, cellIndex: number) => { + const cellValue = cell.isNull || !cell.displayValue ? "NULL" : cell.displayValue; + dataRow[`col${cellIndex}`] = cellValue; + }); + return dataRow; + } + + // Create columns from columnInfo + function createColumns(columnInfo: any[]): Column[] { + // Row number column + const rowNumberColumn: Column = { + id: "rowNumber", + name: '#', + field: "id", + excludeFromColumnPicker: true, + excludeFromGridMenu: true, + excludeFromHeaderMenu: true, + width: 50, + minWidth: 40, + maxWidth: 80, + sortable: false, + resizable: true, + focusable: false, + selectable: false, + formatter: (row: number) => { + const paginationService = reactGridRef.current?.paginationService; + const pageNumber = paginationService?.pageNumber ?? 1; + const itemsPerPage = paginationService?.itemsPerPage ?? pageSize; + const actualRowNumber = (pageNumber - 1) * itemsPerPage + row + 1; + return `${actualRowNumber}`; + }, + }; + + // Data columns + const dataColumns: Column[] = columnInfo.map((colInfo, index) => { + const column: Column = { + id: `col${index}`, + name: colInfo.name, + field: `col${index}`, + sortable: false, + minWidth: 98, // Reduced by 2px to account for border alignment + formatter: ( + _row: number, + cell: number, + value: any, + _columnDef: any, + dataContext: any, + ) => { + const rowId = dataContext.id; + const changeKey = `${rowId}-${cell - 1}`; + const isModified = cellChangesRef.current.has(changeKey); + const hasFailed = failedCellsRef.current.has(changeKey); + const displayValue = value ?? ""; + const isNullValue = displayValue === "NULL"; + + // Safely escape HTML entities (with null/undefined check) + const escapedDisplayValue = + displayValue && typeof displayValue === "string" + ? displayValue + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'") + : String(displayValue || ""); + + const escapedTooltip = escapedDisplayValue; + + // Build CSS classes based on cell state + const cellClasses = []; + if (hasFailed) { + cellClasses.push("table-cell-error"); + } else if (isModified) { + cellClasses.push("table-cell-modified"); + } + + if (isNullValue) { + cellClasses.push("table-cell-null"); + } + + const classAttr = + cellClasses.length > 0 ? ` class="${cellClasses.join(" ")}"` : ""; + + // Failed cells get error styling + if (hasFailed) { + return `
${escapedDisplayValue}
`; + } + // Modified cells get warning styling + if (isModified) { + return `
${escapedDisplayValue}
`; + } + // Normal cells + return `${escapedDisplayValue}`; + }, + }; + + if (colInfo.isEditable) { + column.editor = { + model: Editors.text, + }; + } + + return column; + }); + + return [rowNumberColumn, ...dataColumns]; + } + + // Handle page size changes from props + useEffect(() => { + if (reactGridRef.current?.paginationService && pageSize) { + void reactGridRef.current.paginationService.changeItemPerPage(pageSize); + } + }, [pageSize]); + + // Sync failed cells from props to ref (convert array to Set for fast lookups) + useEffect(() => { + if (failedCells) { + failedCellsRef.current = new Set(failedCells); + // Force grid to re-render to update cell colors + if (reactGridRef.current?.slickGrid) { + reactGridRef.current.slickGrid.invalidate(); + } + } + }, [failedCells]); + + // Handle theme changes - just update state to trigger re-render + useEffect(() => { + if (themeKind !== currentTheme) { + console.log("Theme changed - triggering re-render"); + setCurrentTheme(themeKind); + } + }, [themeKind, currentTheme]); + + // Main effect: Handle resultSet changes + useEffect(() => { + if (!resultSet?.columnInfo || !resultSet?.subset) { + return; + } + + const previousResultSet = previousResultSetRef.current; + const isInitialLoad = !isInitializedRef.current || !previousResultSet; + const columnCountChanged = + previousResultSet?.columnInfo?.length !== resultSet.columnInfo.length; + const rowCountChanged = previousResultSet?.subset?.length !== resultSet.subset.length; + + console.log( + `ResultSet update - Initial: ${isInitialLoad}, Columns changed: ${columnCountChanged}, Rows changed: ${rowCountChanged}`, + ); + + // Scenario 1: Initial load or structural changes - full recreation + if (isInitialLoad || columnCountChanged) { + console.log("Full grid initialization"); + + const newColumns = createColumns(resultSet.columnInfo); + setColumns(newColumns); + + const convertedDataset = resultSet.subset.map(convertRowToDataRow); + setDataset(convertedDataset); + + // Set grid options only on initial load + if (!options) { + // Set row height to 26px for optimal display + const ROW_HEIGHT = 26; + + setOptions({ + enableColumnPicker: false, + enableGridMenu: false, + autoEdit: false, + autoCommitEdit: false, + editable: true, + enableAutoResize: true, + autoResize: { + container: "#grid-container", + bottomPadding: 50, // Reserve space for custom pagination + }, + forceFitColumns: true, + enableColumnReorder: false, + enableHeaderMenu: false, + enableCellNavigation: true, + enableSorting: false, + enableContextMenu: true, + contextMenu: getContextMenuOptions(), + customPaginationComponent: BoundCustomPager, + enablePagination: true, + pagination: { + pageSize: pageSize, + pageSizes: [10, 50, 100, 1000], + }, + editCommandHandler: (_item, _column, editCommand) => { + editCommand.execute(); + }, + rowHeight: ROW_HEIGHT, + darkMode: + themeKind === ColorThemeKind.Dark || + themeKind === ColorThemeKind.HighContrast, + }); + } + } + // Scenario 2: Row count changed (delete/add operations) - incremental add/remove + else if (rowCountChanged && reactGridRef.current?.dataView) { + console.log("Row count changed - applying incremental updates"); + + // Use ID-based comparison instead of position-based + const previousIds = new Set(previousResultSet?.subset?.map((r: any) => r.id) || []); + const currentIds = new Set(resultSet.subset.map((r: any) => r.id)); + + // Add new rows (rows in current but not in previous) + const rowsToAdd = resultSet.subset.filter((row: any) => !previousIds.has(row.id)); + console.log(`Adding ${rowsToAdd.length} new row(s) by ID`); + for (const newRow of rowsToAdd) { + const dataRow = convertRowToDataRow(newRow); + reactGridRef.current.dataView.addItem(dataRow); + } + + // Remove deleted rows (rows in previous but not in current) + const rowsToRemove = (previousResultSet?.subset || []).filter( + (row: any) => !currentIds.has(row.id), + ); + console.log(`Removing ${rowsToRemove.length} deleted row(s) by ID`); + for (const removedRow of rowsToRemove) { + reactGridRef.current.dataView.deleteItem(removedRow.id); + } + + // Refresh grid display + if (reactGridRef.current?.slickGrid) { + reactGridRef.current.slickGrid.invalidate(); + reactGridRef.current.slickGrid.render(); + } + } + // Scenario 3: Row count same - incremental updates only + else if (reactGridRef.current?.dataView) { + console.log("Incremental update - checking for changed rows"); + let hasChanges = false; + + // Check each row for changes + for (let i = 0; i < resultSet.subset.length; i++) { + const newRow = resultSet.subset[i]; + const oldRow = previousResultSet?.subset[i]; + + // Compare row data + if (!oldRow || JSON.stringify(newRow) !== JSON.stringify(oldRow)) { + const dataRow = convertRowToDataRow(newRow); + const existingItem = reactGridRef.current.dataView.getItemById(dataRow.id); + + if (existingItem) { + // Update existing row incrementally + console.log(`Updating row ${dataRow.id} incrementally`); + reactGridRef.current.dataView.updateItem(dataRow.id, dataRow); + hasChanges = true; + } + } + } + + // Only invalidate if there were actual changes + if (hasChanges && reactGridRef.current?.slickGrid) { + reactGridRef.current.slickGrid.invalidate(); + } + } + + previousResultSetRef.current = resultSet; + }, [resultSet, options, themeKind, pageSize]); + + // Restore pagination after dataset changes + useEffect(() => { + if ( + !reactGridRef.current?.paginationService || + !reactGridRef.current?.dataView || + dataset.length === 0 + ) { + return; + } + + const targetPage = lastPageRef.current; + const targetItemsPerPage = lastItemsPerPageRef.current; + const currentPage = reactGridRef.current.paginationService.pageNumber; + const currentItemsPerPage = reactGridRef.current.paginationService.itemsPerPage; + + if (currentItemsPerPage !== targetItemsPerPage) { + console.log(`Restoring items per page to: ${targetItemsPerPage}`); + void reactGridRef.current.paginationService.changeItemPerPage(targetItemsPerPage); + } + + if (targetPage > 1 && currentPage !== targetPage) { + console.log(`Restoring page to: ${targetPage}`); + void reactGridRef.current.paginationService.goToPageNumber(targetPage); + } + }, [dataset]); + + function handleCellChange(_e: CustomEvent, args: any) { + // Capture pagination state + if (reactGridRef.current?.paginationService) { + lastPageRef.current = reactGridRef.current.paginationService.pageNumber; + lastItemsPerPageRef.current = reactGridRef.current.paginationService.itemsPerPage; + } + + const cellIndex = args.cell; + const columnIndex = cellIndex - 1; + const column = columns[cellIndex]; + const rowId = args.item.id; + + console.log(`Cell Changed - Row ID: ${rowId}, Column Index: ${columnIndex}`); + + // Track the change + const changeKey = `${rowId}-${columnIndex}`; + cellChangesRef.current.set(changeKey, { + rowId, + columnIndex, + columnId: column?.id, + field: column?.field, + newValue: args.item[column?.field], + }); + + console.log(`Total changes tracked: ${cellChangesRef.current.size}`); + + // Notify parent of change count update + if (onCellChangeCountChanged) { + onCellChangeCountChanged(cellChangesRef.current.size); + } + + // Notify parent + if (onUpdateCell) { + const newValue = args.item[column?.field]; + onUpdateCell(rowId, columnIndex, newValue); + } + + // Update the display without full re-render + if (reactGridRef.current?.slickGrid) { + reactGridRef.current.slickGrid.invalidate(); + } + } + + function handleContextMenuCommand(_e: any, args: any) { + // Capture pagination state + if (reactGridRef.current?.paginationService) { + lastPageRef.current = reactGridRef.current.paginationService.pageNumber; + lastItemsPerPageRef.current = reactGridRef.current.paginationService.itemsPerPage; + } + + const command = args.command; + const dataContext = args.dataContext; + const rowId = dataContext.id; + + switch (command) { + case "delete-row": + if (onDeleteRow) { + onDeleteRow(rowId); + } + + // Remove tracked changes and failed cells for this row + const keysToDelete: string[] = []; + cellChangesRef.current.forEach((_, key) => { + if (key.startsWith(`${rowId}-`)) { + keysToDelete.push(key); + } + }); + keysToDelete.forEach((key) => { + cellChangesRef.current.delete(key); + failedCellsRef.current.delete(key); + }); + + // Notify parent of change count update + if (onCellChangeCountChanged) { + onCellChangeCountChanged(cellChangesRef.current.size); + } + break; + + case "revert-cell": + const cellIndex = args.cell; + const columnIndex = cellIndex - 1; + const changeKey = `${rowId}-${columnIndex}`; + + if (onRevertCell) { + onRevertCell(rowId, columnIndex); + } + + cellChangesRef.current.delete(changeKey); + failedCellsRef.current.delete(changeKey); + console.log(`Reverted cell for row ID ${rowId}, column ${columnIndex}`); + + // Notify parent of change count update + if (onCellChangeCountChanged) { + onCellChangeCountChanged(cellChangesRef.current.size); + } + break; + + case "revert-row": + if (onRevertRow) { + onRevertRow(rowId); + } + + // Remove tracked changes and failed cells for this row + const keysToDeleteForRevert: string[] = []; + cellChangesRef.current.forEach((_, key) => { + if (key.startsWith(`${rowId}-`)) { + keysToDeleteForRevert.push(key); + } + }); + keysToDeleteForRevert.forEach((key) => { + cellChangesRef.current.delete(key); + failedCellsRef.current.delete(key); + }); + console.log(`Reverted row with ID ${rowId}`); + + // Notify parent of change count update + if (onCellChangeCountChanged) { + onCellChangeCountChanged(cellChangesRef.current.size); + } + break; + } + } + + function getContextMenuOptions(): ContextMenu { + return { + hideCopyCellValueCommand: true, + hideCloseButton: true, + commandItems: [ + { + command: "delete-row", + title: loc.tableExplorer.deleteRow, + iconCssClass: "mdi mdi-close", + cssClass: "red", + textCssClass: "bold", + positionOrder: 1, + }, + { + command: "revert-cell", + title: loc.tableExplorer.revertCell, + iconCssClass: "mdi mdi-undo", + positionOrder: 2, + }, + { + command: "revert-row", + title: loc.tableExplorer.revertRow, + iconCssClass: "mdi mdi-undo", + positionOrder: 3, + }, + ], + onCommand: (e, args) => handleContextMenuCommand(e, args), + }; + } + + if (!resultSet || columns.length === 0 || !options) { + return null; + } + + const isDarkMode = + currentTheme === ColorThemeKind.Dark || currentTheme === ColorThemeKind.HighContrast; + + return ( +
+ reactGridReady($event.detail)} + onCellChange={($event) => handleCellChange($event, $event.detail.args)} + /> +
+ ); + }, +); diff --git a/src/reactviews/pages/TableExplorer/TableExplorerCustomPager.css b/src/reactviews/pages/TableExplorer/TableExplorerCustomPager.css new file mode 100644 index 0000000000..acfc734388 --- /dev/null +++ b/src/reactviews/pages/TableExplorer/TableExplorerCustomPager.css @@ -0,0 +1,223 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +.table-explorer-custom-pagination { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 16px; + padding: 8px 12px; + background-color: var(--vscode-editor-background); + border-top: 1px solid var(--vscode-panel-border); + font-size: 13px; + color: var(--vscode-foreground); + user-select: none; + flex-shrink: 0; + margin-top: 0; +} + +.table-explorer-custom-pagination .row-count-selector { + display: flex; + align-items: center; + gap: 8px; + margin-right: 16px; +} + +.table-explorer-custom-pagination .row-count-selector .fui-Combobox { + position: relative; + z-index: 10; +} + +.table-explorer-custom-pagination .row-count-selector .fui-Button { + min-width: 32px; + height: 28px; + padding: 0 8px; + margin-left: 4px; + z-index: 11; + position: relative; +} + +.table-explorer-custom-pagination .row-count-selector .row-count-label { + color: var(--vscode-descriptionForeground); + white-space: nowrap; + font-size: 12px; +} + +.table-explorer-custom-pagination .pagination-info { + display: flex; + align-items: center; + gap: 4px; +} + +.table-explorer-custom-pagination .pagination-count { + display: flex; + align-items: center; + gap: 4px; +} + +.table-explorer-custom-pagination .page-size-selector { + display: flex; + align-items: center; + gap: 8px; +} + +.table-explorer-custom-pagination .page-size-label { + color: var(--vscode-descriptionForeground); + white-space: nowrap; + font-size: 12px; +} + +.table-explorer-custom-pagination .page-from-to { + display: flex; + align-items: center; + gap: 4px; +} + +.table-explorer-custom-pagination .page-from-to .separator { + color: var(--vscode-descriptionForeground); +} + +.table-explorer-custom-pagination .total-items { + font-weight: 500; +} + +.table-explorer-custom-pagination .items-label { + color: var(--vscode-descriptionForeground); +} + +.table-explorer-custom-pagination .pagination-controls { + display: flex; + align-items: center; + gap: 12px; +} + +.table-explorer-custom-pagination .pagination-nav { + display: flex; + align-items: center; + gap: 2px; +} + +.table-explorer-custom-pagination .page-info { + display: flex; + align-items: center; + gap: 6px; + padding: 0 8px; +} + +.table-explorer-custom-pagination .page-info .page-label, +.table-explorer-custom-pagination .page-info .separator { + color: var(--vscode-descriptionForeground); +} + +.table-explorer-custom-pagination .page-info .page-number { + font-weight: 500; + min-width: 20px; + text-align: center; +} + +.table-explorer-custom-pagination .page-info .page-count { + min-width: 20px; + text-align: center; +} + +.table-explorer-custom-pagination .pagination-button { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + padding: 0; + background-color: transparent; + border: none; + border-radius: 3px; + color: var(--vscode-icon-foreground); + cursor: pointer; + transition: background-color 0.1s ease; +} + +.table-explorer-custom-pagination .pagination-button svg { + width: 16px; + height: 16px; +} + +.table-explorer-custom-pagination .pagination-button:hover:not(:disabled) { + background-color: var(--vscode-toolbar-hoverBackground); +} + +.table-explorer-custom-pagination .pagination-button:active:not(:disabled) { + background-color: var(--vscode-toolbar-activeBackground); +} + +.table-explorer-custom-pagination .pagination-button:disabled { + color: var(--vscode-disabledForeground); + cursor: not-allowed; + opacity: 0.4; +} + +/* Constrain dropdown widths */ +.table-explorer-custom-pagination .row-count-selector .fui-Dropdown, +.table-explorer-custom-pagination .row-count-selector .fui-Combobox { + width: 80px; + min-width: 80px; + max-width: 80px; +} + +.table-explorer-custom-pagination .row-count-selector .fui-Combobox > input { + width: 100%; +} + +.table-explorer-custom-pagination .page-size-selector .fui-Dropdown { + width: 70px; + min-width: 70px; + max-width: 70px; +} + +/* Fix dropdown portal styling - target actual Fluent UI classes from DOM */ +.fui-Listbox.fui-Dropdown__listbox { + background-color: var(--vscode-dropdown-background); + border: 1px solid var(--vscode-dropdown-border); + box-shadow: var(--vscode-widget-shadow); + z-index: 1000; + min-width: 120px; +} + +.fui-Option { + background-color: var(--vscode-dropdown-background); + color: var(--vscode-dropdown-foreground); +} + +.fui-Option:hover { + background-color: var(--vscode-list-hoverBackground); + color: var(--vscode-list-hoverForeground); +} + +.fui-Option[aria-selected="true"] { + background-color: var(--vscode-list-activeSelectionBackground); + color: var(--vscode-list-activeSelectionForeground); +} + +/* Global fallback selectors for dropdown portals (covers both Dropdown and Combobox) */ +div[role="listbox"][id^="fluent-listbox"] { + background-color: var(--vscode-dropdown-background); + border: 1px solid var(--vscode-dropdown-border); + box-shadow: var(--vscode-widget-shadow); + z-index: 1000; + min-width: 120px; +} + +div[role="option"][class*="fui-Option"] { + background-color: var(--vscode-dropdown-background); + color: var(--vscode-dropdown-foreground); +} + +div[role="option"][class*="fui-Option"]:hover { + background-color: var(--vscode-list-hoverBackground); + color: var(--vscode-list-hoverForeground); +} + +div[role="option"][class*="fui-Option"][aria-selected="true"] { + background-color: var(--vscode-list-activeSelectionBackground); + color: var(--vscode-list-activeSelectionForeground); +} diff --git a/src/reactviews/pages/TableExplorer/TableExplorerCustomPager.tsx b/src/reactviews/pages/TableExplorer/TableExplorerCustomPager.tsx new file mode 100644 index 0000000000..2410a8f555 --- /dev/null +++ b/src/reactviews/pages/TableExplorer/TableExplorerCustomPager.tsx @@ -0,0 +1,288 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { + PaginationMetadata, + PaginationService, + PubSubService, + SlickGrid, + Subscription, +} from "@slickgrid-universal/common"; +import React, { useEffect, useImperativeHandle, useRef, useState } from "react"; +import { + ChevronLeftRegular, + ChevronRightRegular, + ChevronDoubleLeftRegular, + ChevronDoubleRightRegular, + ArrowSyncRegular, +} from "@fluentui/react-icons"; +import { Dropdown, Option, Combobox, Button } from "@fluentui/react-components"; + +import "./TableExplorerCustomPager.css"; +import { locConstants as loc } from "../../common/locConstants"; + +// Default pagination constants +const DEFAULT_PAGE_SIZE = 100; +const DEFAULT_ROW_COUNT = 100; +const MIN_VALID_NUMBER = 1; +const FIRST_PAGE_NUMBER = 1; +const RADIX_DECIMAL = 10; + +export interface TableExplorerCustomPagerRef { + init: ( + grid: SlickGrid, + paginationService: PaginationService, + pubSubService: PubSubService, + ) => void; + dispose: () => void; + renderPagination: () => void; +} + +export interface TableExplorerCustomPagerProps { + currentRowCount?: number; + onLoadSubset?: (rowCount: number) => void; +} + +const TableExplorerCustomPager = React.forwardRef< + TableExplorerCustomPagerRef, + TableExplorerCustomPagerProps +>((props, ref) => { + const { currentRowCount, onLoadSubset } = props; + const [currentPagination, setCurrentPagination] = useState( + {} as PaginationMetadata, + ); + const [isLeftPaginationDisabled, setIsLeftPaginationDisabled] = useState(false); + const [isRightPaginationDisabled, setIsRightPaginationDisabled] = useState(false); + const [selectedPageSize, setSelectedPageSize] = useState(String(DEFAULT_PAGE_SIZE)); + const [selectedRowCount, setSelectedRowCount] = useState(String(DEFAULT_ROW_COUNT)); + + const paginationElementRef = useRef(null); + const gridRef = useRef(null); + const paginationServiceRef = useRef(null); + const pubSubServiceRef = useRef(null); + const subscriptionsRef = useRef([]); + + const checkLeftPaginationDisabled = (pagination: PaginationMetadata): boolean => { + return pagination.pageNumber === FIRST_PAGE_NUMBER || pagination.totalItems === 0; + }; + + const checkRightPaginationDisabled = (pagination: PaginationMetadata): boolean => { + return pagination.pageNumber === pagination.pageCount || pagination.totalItems === 0; + }; + + const init = ( + grid: SlickGrid, + paginationService: PaginationService, + pubSubService: PubSubService, + ) => { + gridRef.current = grid; + paginationServiceRef.current = paginationService; + pubSubServiceRef.current = pubSubService; + + const currentPagination = paginationService.getFullPagination(); + setCurrentPagination(currentPagination); + setIsLeftPaginationDisabled(checkLeftPaginationDisabled(currentPagination)); + setIsRightPaginationDisabled(checkRightPaginationDisabled(currentPagination)); + setSelectedPageSize(String(currentPagination.pageSize || DEFAULT_PAGE_SIZE)); + + const subscription = pubSubService.subscribe( + "onPaginationRefreshed", + (paginationChanges) => { + setCurrentPagination(paginationChanges); + setIsLeftPaginationDisabled(checkLeftPaginationDisabled(paginationChanges)); + setIsRightPaginationDisabled(checkRightPaginationDisabled(paginationChanges)); + setSelectedPageSize(String(paginationChanges.pageSize || DEFAULT_PAGE_SIZE)); + }, + ); + + subscriptionsRef.current.push(subscription); + }; + + const dispose = () => { + pubSubServiceRef.current?.unsubscribeAll(subscriptionsRef.current); + paginationElementRef.current?.remove(); + }; + + const renderPagination = () => { + if (paginationServiceRef.current) { + const currentPagination = paginationServiceRef.current.getFullPagination(); + setCurrentPagination(currentPagination); + setIsLeftPaginationDisabled(checkLeftPaginationDisabled(currentPagination)); + setIsRightPaginationDisabled(checkRightPaginationDisabled(currentPagination)); + setSelectedPageSize(String(currentPagination.pageSize || DEFAULT_PAGE_SIZE)); + } + }; + + const onFirstPageClicked = (event: any) => { + if (!checkLeftPaginationDisabled(currentPagination)) { + void paginationServiceRef.current?.goToFirstPage(event); + } + }; + + const onLastPageClicked = (event: any) => { + if (!checkRightPaginationDisabled(currentPagination)) { + void paginationServiceRef.current?.goToLastPage(event); + } + }; + + const onNextPageClicked = (event: any) => { + if (!checkRightPaginationDisabled(currentPagination)) { + void paginationServiceRef.current?.goToNextPage(event); + } + }; + + const onPreviousPageClicked = (event: any) => { + if (!checkLeftPaginationDisabled(currentPagination)) { + void paginationServiceRef.current?.goToPreviousPage(event); + } + }; + + const onPageSizeChanged = (_event: any, data: any) => { + const newPageSize = data.optionValue || data.value; + setSelectedPageSize(newPageSize); + const pageSizeNumber = parseInt(newPageSize, RADIX_DECIMAL); + + if (!isNaN(pageSizeNumber) && pageSizeNumber >= MIN_VALID_NUMBER) { + void paginationServiceRef.current?.changeItemPerPage(pageSizeNumber); + } + }; + + const onRowCountChanged = (_event: any, data: any) => { + const newRowCount = data.optionValue || data.value || selectedRowCount; + if (newRowCount) { + setSelectedRowCount(newRowCount); + } + }; + + const onRowCountInput = (event: React.ChangeEvent) => { + const newValue = event.target.value; + setSelectedRowCount(newValue); + }; + + const onFetchRowsClick = () => { + const rowCountNumber = parseInt( + selectedRowCount || String(DEFAULT_ROW_COUNT), + RADIX_DECIMAL, + ); + + if (!isNaN(rowCountNumber) && rowCountNumber >= MIN_VALID_NUMBER && onLoadSubset) { + onLoadSubset(rowCountNumber); + } + }; + + useEffect(() => { + return () => { + dispose(); + }; + }, []); + + useEffect(() => { + if (currentRowCount !== undefined) { + setSelectedRowCount(String(currentRowCount)); + } + }, [currentRowCount]); + + // Expose methods via ref + useImperativeHandle(ref, () => ({ + init, + dispose, + renderPagination, + })); + + return ( +
+
+ {loc.tableExplorer.totalRowsToFetch} + + + + + + + +
+
+ {loc.tableExplorer.rowsPerPage} + + + + + + +
+
+
+ + +
+
+ + {currentPagination.dataFrom || 0} + + - + + {currentPagination.dataTo || 0} + + of + + {currentPagination.totalItems || 0} + +
+
+ + +
+
+
+ ); +}); + +export default TableExplorerCustomPager; diff --git a/src/reactviews/pages/TableExplorer/TableExplorerPage.tsx b/src/reactviews/pages/TableExplorer/TableExplorerPage.tsx new file mode 100644 index 0000000000..e7ed137f2e --- /dev/null +++ b/src/reactviews/pages/TableExplorer/TableExplorerPage.tsx @@ -0,0 +1,125 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React, { useRef } from "react"; +import { useTableExplorerContext } from "./TableExplorerStateProvider"; +import { TableDataGrid, TableDataGridRef } from "./TableDataGrid"; +import { TableExplorerToolbar } from "./TableExplorerToolbar"; +import { + DesignerDefinitionPane, + DesignerDefinitionTabs, +} from "../../common/designerDefinitionPane"; +import { makeStyles, shorthands } from "@fluentui/react-components"; +import { locConstants as loc } from "../../common/locConstants"; +import { useTableExplorerSelector } from "./tableExplorerSelector"; +import { useVscodeWebview2 } from "../../common/vscodeWebviewProvider2"; +import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; + +const useStyles = makeStyles({ + root: { + display: "flex", + flexDirection: "column", + height: "100vh", + width: "100%", + ...shorthands.overflow("hidden"), + }, + panelGroup: { + ...shorthands.flex(1), + width: "100%", + height: "100%", + }, + contentArea: { + ...shorthands.flex(1), + display: "flex", + flexDirection: "column", + ...shorthands.overflow("hidden"), + padding: "20px", + height: "100%", + }, + dataGridContainer: { + ...shorthands.flex(1), + ...shorthands.overflow("auto"), + minHeight: 0, + }, + resizeHandle: { + height: "2px", + backgroundColor: "var(--vscode-editorWidget-border)", + }, +}); + +export const TableExplorerPage: React.FC = () => { + const classes = useStyles(); + const context = useTableExplorerContext(); + const { themeKind } = useVscodeWebview2(); + + // Use selectors to access specific state properties + const resultSet = useTableExplorerSelector((s) => s.resultSet); + const isLoading = useTableExplorerSelector((s) => s.isLoading); + const currentRowCount = useTableExplorerSelector((s) => s.currentRowCount); + const failedCells = useTableExplorerSelector((s) => s.failedCells); + const showScriptPane = useTableExplorerSelector((s) => s.showScriptPane); + const updateScript = useTableExplorerSelector((s) => s.updateScript); + + const gridRef = useRef(null); + const [cellChangeCount, setCellChangeCount] = React.useState(0); + + const handleSaveComplete = () => { + // Clear the change tracking in the grid after successful save + gridRef.current?.clearAllChangeTracking(); + }; + + const handleCellChangeCountChanged = (count: number) => { + setCellChangeCount(count); + }; + + return ( +
+ + +
+ + {resultSet ? ( +
+ +
+ ) : isLoading ? ( +

{loc.tableExplorer.loadingTableData}

+ ) : ( +

{loc.tableExplorer.noDataAvailable}

+ )} +
+
+ {showScriptPane && ( + <> + + context.openScriptInEditor()} + copyToClipboard={() => context.copyScriptToClipboard()} + activeTab={DesignerDefinitionTabs.Script} + /> + + )} +
+
+ ); +}; diff --git a/src/reactviews/pages/TableExplorer/TableExplorerStateProvider.tsx b/src/reactviews/pages/TableExplorer/TableExplorerStateProvider.tsx new file mode 100644 index 0000000000..71ccbcfc30 --- /dev/null +++ b/src/reactviews/pages/TableExplorer/TableExplorerStateProvider.tsx @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React, { createContext, useContext, useMemo } from "react"; +import { + TableExplorerWebViewState, + TableExplorerReducers, + TableExplorerContextProps, +} from "../../../sharedInterfaces/tableExplorer"; +import { useVscodeWebview2 } from "../../common/vscodeWebviewProvider2"; +import { getCoreRPCs2 } from "../../common/utils"; + +const TableExplorerContext = createContext( + {} as TableExplorerContextProps, +); + +export const TableExplorerStateProvider: React.FC<{ + children: React.ReactNode; +}> = ({ children }) => { + const { extensionRpc } = useVscodeWebview2(); + + const commands = useMemo( + () => ({ + ...getCoreRPCs2(extensionRpc), + commitChanges: function (): void { + extensionRpc.action("commitChanges", {}); + }, + + loadSubset: function (rowCount: number): void { + extensionRpc.action("loadSubset", { rowCount }); + }, + + createRow: function (): void { + extensionRpc.action("createRow", {}); + }, + + deleteRow: function (rowId: number): void { + extensionRpc.action("deleteRow", { rowId }); + }, + + updateCell: function (rowId: number, columnId: number, newValue: string): void { + extensionRpc.action("updateCell", { rowId, columnId, newValue }); + }, + + revertCell: function (rowId: number, columnId: number): void { + extensionRpc.action("revertCell", { rowId, columnId }); + }, + + revertRow: function (rowId: number): void { + extensionRpc.action("revertRow", { rowId }); + }, + + generateScript: function (): void { + extensionRpc.action("generateScript", {}); + }, + + openScriptInEditor: function (): void { + extensionRpc.action("openScriptInEditor", {}); + }, + + copyScriptToClipboard: function (): void { + extensionRpc.action("copyScriptToClipboard", {}); + }, + + toggleScriptPane: function (): void { + extensionRpc.action("toggleScriptPane", {}); + }, + + setCurrentPage: function (pageNumber: number): void { + extensionRpc.action("setCurrentPage", { pageNumber }); + }, + }), + [extensionRpc], + ); + + return ( + {children} + ); +}; + +export const useTableExplorerContext = (): TableExplorerContextProps => { + const context = useContext(TableExplorerContext); + return context; +}; diff --git a/src/reactviews/pages/TableExplorer/TableExplorerToolbar.tsx b/src/reactviews/pages/TableExplorer/TableExplorerToolbar.tsx new file mode 100644 index 0000000000..c80c2eed55 --- /dev/null +++ b/src/reactviews/pages/TableExplorer/TableExplorerToolbar.tsx @@ -0,0 +1,82 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import React from "react"; +import { Toolbar, ToolbarButton } from "@fluentui/react-components"; +import { SaveRegular, AddRegular, CodeRegular } from "@fluentui/react-icons"; +import { locConstants as loc } from "../../common/locConstants"; +import { useTableExplorerContext } from "./TableExplorerStateProvider"; +import { useTableExplorerSelector } from "./tableExplorerSelector"; + +interface TableExplorerToolbarProps { + onSaveComplete?: () => void; + cellChangeCount: number; +} + +export const TableExplorerToolbar: React.FC = ({ + onSaveComplete, + cellChangeCount, +}) => { + const context = useTableExplorerContext(); + + // Use selectors to access state + const showScriptPane = useTableExplorerSelector((s) => s.showScriptPane); + + const handleSave = () => { + context.commitChanges(); + // Call the callback to clear change tracking after save + if (onSaveComplete) { + onSaveComplete(); + } + }; + + const handleAddRow = () => { + context.createRow(); + }; + + // Use cell-level change count directly + // This provides accurate granularity: each cell edit counts as one change + const changeCount = cellChangeCount; + + const saveButtonText = + changeCount > 0 + ? `${loc.tableExplorer.saveChanges} (${changeCount})` + : loc.tableExplorer.saveChanges; + + return ( + + } + onClick={handleSave} + disabled={changeCount === 0}> + {saveButtonText} + + } + onClick={handleAddRow}> + {loc.tableExplorer.addRow} + + } + onClick={() => { + if (showScriptPane) { + context.toggleScriptPane(); + } else { + context.generateScript(); + } + }}> + {showScriptPane ? loc.tableExplorer.hideScript : loc.tableExplorer.showScript} + + + ); +}; diff --git a/src/reactviews/pages/TableExplorer/index.tsx b/src/reactviews/pages/TableExplorer/index.tsx new file mode 100644 index 0000000000..ad15165f0a --- /dev/null +++ b/src/reactviews/pages/TableExplorer/index.tsx @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import ReactDOM from "react-dom/client"; +import "../../index.css"; +import { VscodeWebviewProvider2 } from "../../common/vscodeWebviewProvider2"; +import { TableExplorerPage } from "./TableExplorerPage"; +import { TableExplorerStateProvider } from "./TableExplorerStateProvider"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + , +); diff --git a/src/reactviews/pages/TableExplorer/tableExplorerSelector.ts b/src/reactviews/pages/TableExplorer/tableExplorerSelector.ts new file mode 100644 index 0000000000..a7c499d822 --- /dev/null +++ b/src/reactviews/pages/TableExplorer/tableExplorerSelector.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { + TableExplorerReducers, + TableExplorerWebViewState, +} from "../../../sharedInterfaces/tableExplorer"; +import { useVscodeSelector } from "../../common/useVscodeSelector"; + +export function useTableExplorerSelector( + selector: (state: TableExplorerWebViewState) => T, + equals: (a: T, b: T) => boolean = Object.is, +) { + return useVscodeSelector(selector, equals); +} diff --git a/src/services/tableExplorerService.ts b/src/services/tableExplorerService.ts new file mode 100644 index 0000000000..d8ff3cef18 --- /dev/null +++ b/src/services/tableExplorerService.ts @@ -0,0 +1,444 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import SqlToolsServiceClient from "../languageservice/serviceclient"; +import { + EditCommitRequest, + EditCreateRowRequest, + EditDeleteRowRequest, + EditDisposeRequest, + EditInitializeRequest, + EditRevertCellRequest, + EditRevertRowRequest, + EditScriptRequest, + EditSubsetRequest, + EditUpdateCellRequest, +} from "../models/contracts/tableExplorer"; +import { + EditCommitParams, + EditCommitResult, + EditCreateRowParams, + EditCreateRowResult, + EditDeleteRowParams, + EditDeleteRowResult, + EditDisposeParams, + EditDisposeResult, + EditInitializeFiltering, + EditInitializeParams, + EditInitializeResult, + EditRevertCellParams, + EditRevertCellResult, + EditRevertRowParams, + EditRevertRowResult, + EditScriptParams, + EditScriptResult, + EditSubsetParams, + EditSubsetResult, + EditUpdateCellParams, + EditUpdateCellResult, +} from "../sharedInterfaces/tableExplorer"; +import { getErrorMessage } from "../utils/utils"; + +/** + * Interface for the Table Explorer Service that handles table editing operations. + */ +export interface ITableExplorerService { + /** + * Gets the SQL Tools Service client instance. + */ + readonly sqlToolsClient: SqlToolsServiceClient; + + /** + * Initializes the table explorer service with the specified parameters. + * + * @param ownerUri - The URI identifying the owner/connection for the table + * @param objectName - The name of the database object (table, view, etc.) + * @param schemaName - The schema name containing the object + * @param objectType - The type of database object being explored + * @param queryString - Optional query string for filtering or custom queries + * @param limitResults - Optional limit on the number of results to return + * @returns A Promise that resolves to an EditInitializeResult containing initialization data + */ + initialize( + ownerUri: string, + objectName: string, + schemaName: string, + objectType: string, + queryString: string | undefined, + limitResults?: number | undefined, + ): Promise; + + /** + * Retrieves a subset of rows from a table or query result set. + * + * @param ownerUri - The unique identifier for the connection or query session + * @param rowStartIndex - The zero-based index of the first row to retrieve + * @param rowCount - The number of rows to retrieve starting from the start index + * @returns A promise that resolves to an EditSubsetResult containing the requested subset of data + */ + subset(ownerUri: string, rowStartIndex: number, rowCount: number): Promise; + + /** + * Commits pending changes for the specified owner URI. + * + * @param ownerUri - The unique identifier for the resource owner + * @returns A promise that resolves to the commit result containing operation status and details + */ + commit(ownerUri: string): Promise; + + /** + * Creates a new row for editing in the specified table. + * + * @param ownerUri - The URI identifying the connection and table context + * @returns A Promise that resolves to the result of the create row operation + */ + createRow(ownerUri: string): Promise; + + /** + * Deletes a row from a table in the database. + * + * @param ownerUri - The URI identifying the connection and database context + * @param rowId - The unique identifier of the row to be deleted + * @returns A promise that resolves to the result of the delete operation + */ + deleteRow(ownerUri: string, rowId: number): Promise; + + /** + * Reverts a row to its original state by discarding any pending changes. + * + * @param ownerUri - The unique identifier for the connection/document owner + * @param rowId - The identifier of the row to revert + * @returns A promise that resolves to the result of the revert operation + */ + revertRow(ownerUri: string, rowId: number): Promise; + + /** + * Updates a single cell value in a table row. + * + * @param ownerUri - The URI identifier for the database connection or table owner + * @param rowId - The identifier of the row containing the cell to update + * @param columnId - The identifier of the column containing the cell to update + * @param newValue - The new value to set for the specified cell + * @returns A promise that resolves to the result of the cell update operation + */ + updateCell( + ownerUri: string, + rowId: number, + columnId: number, + newValue: string, + ): Promise; + + /** + * Reverts a cell in the table editor to its original value. + * + * @param ownerUri - The unique identifier for the database connection/document + * @param rowId - The identifier of the row containing the cell to revert + * @param columnId - The identifier of the column containing the cell to revert + * @returns A promise that resolves to the result of the revert cell operation + */ + revertCell(ownerUri: string, rowId: number, columnId: number): Promise; + + /** + * Disposes of resources associated with the specified owner URI. + * + * @param ownerUri - The URI of the owner whose resources should be disposed + * @returns A promise that resolves to the dispose result containing cleanup status + */ + dispose(ownerUri: string): Promise; + + /** + * Generates update scripts for the specified owner URI based on changes made during + * the edit session. + * + * @param ownerUri - The URI identifying the owner for which to generate scripts + * @returns A promise that resolves to an EditScriptResult containing the generated scripts + */ + generateScripts(ownerUri: string): Promise; +} + +export class TableExplorerService implements ITableExplorerService { + constructor(private _client: SqlToolsServiceClient) {} + + /** + * Gets the SQL Tools Service client instance. + * @returns {SqlToolsServiceClient} The SQL Tools Service client used for database operations. + */ + public get sqlToolsClient(): SqlToolsServiceClient { + return this._client; + } + + /** + * Initializes the table explorer service with the specified parameters. + * + * @param ownerUri - The URI identifying the owner/connection for the table + * @param objectName - The name of the database object (table, view, etc.) + * @param schemaName - The schema name containing the object + * @param objectType - The type of database object being explored + * @param queryString - Optional query string for filtering or custom queries + * @param limitResults - Optional limit on the number of results to return + * @returns A Promise that resolves to an EditInitializeResult containing initialization data + * @throws Logs error and re-throws if the initialization request fails + */ + public async initialize( + ownerUri: string, + objectName: string, + schemaName: string, + objectType: string, + queryString: string | undefined, + limitResults?: number | undefined, + ): Promise { + try { + const filters: EditInitializeFiltering = { + LimitResults: limitResults, + }; + + const params: EditInitializeParams = { + ownerUri: ownerUri, + filters: filters, + objectName: objectName, + schemaName: schemaName, + objectType: objectType, + queryString: queryString, + }; + + const result = await this._client.sendRequest(EditInitializeRequest.type, params); + + return result; + } catch (error) { + this._client.logger.error(getErrorMessage(error)); + throw error; + } + } + + /** + * Retrieves a subset of rows from a table or query result set. + * + * @param ownerUri - The unique identifier for the connection or query session + * @param rowStartIndex - The zero-based index of the first row to retrieve + * @param rowCount - The number of rows to retrieve starting from the start index + * @returns A promise that resolves to an EditSubsetResult containing the requested subset of data + * @throws Will throw an error if the subset request fails or if there are connection issues + */ + public async subset( + ownerUri: string, + rowStartIndex: number, + rowCount: number, + ): Promise { + try { + const params: EditSubsetParams = { + ownerUri: ownerUri, + rowStartIndex: rowStartIndex, + rowCount: rowCount, + }; + + const result = await this._client.sendRequest(EditSubsetRequest.type, params); + + return result; + } catch (error) { + this._client.logger.error(getErrorMessage(error)); + throw error; + } + } + + /** + * Commits pending changes for the specified owner URI. + * + * @param ownerUri - The unique identifier for the resource owner + * @returns A promise that resolves to the commit result containing operation status and details + * @throws Will throw an error if the commit operation fails or if there are communication issues with the client + */ + public async commit(ownerUri: string): Promise { + try { + const params: EditCommitParams = { + ownerUri: ownerUri, + }; + + const result = await this._client.sendRequest(EditCommitRequest.type, params); + + return result; + } catch (error) { + this._client.logger.error(getErrorMessage(error)); + throw error; + } + } + + /** + * Creates a new row for editing in the specified table. + * + * @param ownerUri - The URI identifying the connection and table context + * @returns A Promise that resolves to the result of the create row operation + * @throws Will throw an error if the create row request fails + */ + public async createRow(ownerUri: string): Promise { + try { + const params: EditCreateRowParams = { + ownerUri: ownerUri, + }; + + const result = await this._client.sendRequest(EditCreateRowRequest.type, params); + + return result; + } catch (error) { + this._client.logger.error(getErrorMessage(error)); + throw error; + } + } + + /** + * Deletes a row from a table in the database. + * + * @param ownerUri - The URI identifying the connection and database context + * @param rowId - The unique identifier of the row to be deleted + * @returns A promise that resolves to the result of the delete operation + * @throws Will throw an error if the delete operation fails or if there are connection issues + */ + public async deleteRow(ownerUri: string, rowId: number): Promise { + try { + const params: EditDeleteRowParams = { + ownerUri: ownerUri, + rowId: rowId, + }; + + const result = await this._client.sendRequest(EditDeleteRowRequest.type, params); + + return result; + } catch (error) { + this._client.logger.error(getErrorMessage(error)); + throw error; + } + } + + /** + * Reverts a row to its original state by discarding any pending changes. + * + * @param ownerUri - The unique identifier for the connection/document owner + * @param rowId - The identifier of the row to revert + * @returns A promise that resolves to the result of the revert operation + * @throws Will throw an error if the revert operation fails or if there are communication issues with the client + */ + public async revertRow(ownerUri: string, rowId: number): Promise { + try { + const params: EditRevertRowParams = { + ownerUri: ownerUri, + rowId: rowId, + }; + + const result = await this._client.sendRequest(EditRevertRowRequest.type, params); + + return result; + } catch (error) { + this._client.logger.error(getErrorMessage(error)); + throw error; + } + } + + /** + * Updates a single cell value in a table row. + * + * @param ownerUri - The URI identifier for the database connection or table owner + * @param rowId - The identifier of the row containing the cell to update + * @param columnId - The identifier of the column containing the cell to update + * @param newValue - The new value to set for the specified cell + * @returns A promise that resolves to the result of the cell update operation + * @throws Will throw an error if the update operation fails or if there are communication issues with the client + */ + public async updateCell( + ownerUri: string, + rowId: number, + columnId: number, + newValue: string, + ): Promise { + try { + const params: EditUpdateCellParams = { + ownerUri: ownerUri, + rowId: rowId, + columnId: columnId, + newValue: newValue, + }; + + const result = await this._client.sendRequest(EditUpdateCellRequest.type, params); + + return result; + } catch (error) { + this._client.logger.error(getErrorMessage(error)); + throw error; + } + } + + /** + * Reverts a cell in the table editor to its original value. + * + * @param ownerUri - The unique identifier for the database connection/document + * @param rowId - The identifier of the row containing the cell to revert + * @param columnId - The identifier of the column containing the cell to revert + * @returns A promise that resolves to the result of the revert cell operation + * @throws Will throw an error if the revert operation fails or if there's a communication issue with the client + */ + public async revertCell( + ownerUri: string, + rowId: number, + columnId: number, + ): Promise { + try { + const params: EditRevertCellParams = { + ownerUri: ownerUri, + rowId: rowId, + columnId: columnId, + }; + + const result = await this._client.sendRequest(EditRevertCellRequest.type, params); + + return result; + } catch (error) { + this._client.logger.error(getErrorMessage(error)); + throw error; + } + } + + /** + * Disposes of resources associated with the specified owner URI. + * + * @param ownerUri - The URI of the owner whose resources should be disposed + * @returns A promise that resolves to the dispose result containing cleanup status + * @throws Will throw an error if the dispose request fails or if there's a communication error with the client + */ + public async dispose(ownerUri: string): Promise { + try { + const params: EditDisposeParams = { + ownerUri: ownerUri, + }; + + const result = await this._client.sendRequest(EditDisposeRequest.type, params); + + return result; + } catch (error) { + this._client.logger.error(getErrorMessage(error)); + throw error; + } + } + + /** + * Generates update scripts for the specified owner URI based on changes made during + * the edit session. + * + * @param ownerUri - The URI identifying the owner for which to generate scripts + * @returns A promise that resolves to an EditScriptResult containing the generated scripts + * @throws Will throw an error if the script generation request fails + */ + public async generateScripts(ownerUri: string): Promise { + try { + const params: EditScriptParams = { + ownerUri: ownerUri, + }; + + const result = await this._client.sendRequest(EditScriptRequest.type, params); + + return result; + } catch (error) { + this._client.logger.error(getErrorMessage(error)); + throw error; + } + } +} diff --git a/src/sharedInterfaces/tableExplorer.ts b/src/sharedInterfaces/tableExplorer.ts new file mode 100644 index 0000000000..b45e1173a8 --- /dev/null +++ b/src/sharedInterfaces/tableExplorer.ts @@ -0,0 +1,221 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export interface IEditSessionOperationParams { + ownerUri: string; +} + +export interface EditInitializeFiltering { + LimitResults?: number | undefined; +} + +export enum EditRowState { + clean = 0, + dirtyInsert = 1, + dirtyDelete = 2, + dirtyUpdate = 3, +} + +export interface EditRow { + cells: DbCellValue[]; + id: number; + isDirty: boolean; + state: EditRowState; +} + +export interface DbCellValue { + displayValue: string; + isNull: boolean; + invariantCultureDisplayValue: string; +} + +export interface IEditRowOperationParams extends IEditSessionOperationParams { + rowId: number; +} + +export interface EditCell extends DbCellValue { + isDirty: boolean; +} +export interface EditCellResult { + cell: EditCell; + isRowDirty: boolean; +} + +export interface EditReferencedTableInfo { + schemaName: string; + tableName: string; + fullyQualifiedName: string; + foreignKeyName: string; + sourceColumns: string[]; + ReferencedColumns: string[]; +} + +//#region edit/initialize + +export interface EditInitializeParams extends IEditSessionOperationParams { + filters: EditInitializeFiltering; + objectName: string; + schemaName: string; + objectType: string; + queryString: string; +} + +export interface EditInitializeResult {} + +//#endregion + +//#region edit/sessionReady Event + +export interface EditSessionReadyParams { + ownerUri: string; + success: boolean; + message: string; +} + +//#endregion + +//#region edit/subset + +export interface EditSubsetParams extends IEditSessionOperationParams { + rowStartIndex: number; + rowCount: number; +} + +export interface EditColumnInfo { + name: string; + isEditable: boolean; +} + +export interface EditSubsetResult { + rowCount: number; + subset: EditRow[]; + columnInfo: EditColumnInfo[]; +} + +//#endregion + +//#region edit/commit + +export interface EditCommitParams extends IEditSessionOperationParams {} + +export interface EditCommitResult {} + +//#endregion + +//#region edit/createRow + +export interface EditCreateRowParams extends IEditSessionOperationParams {} + +export interface EditCreateRowResult { + defaultValues: string[]; + newRowId: number; + row: EditRow; +} + +//#endregion + +//#region edit/deleteRow + +export interface EditDeleteRowParams extends IEditRowOperationParams {} + +export interface EditDeleteRowResult {} + +//#endregion + +//#region edit/revertRow + +export interface EditRevertRowParams extends IEditRowOperationParams {} + +export interface EditRevertRowResult { + row: EditRow; +} + +//#endregion + +//#region edit/updateCell + +export interface EditUpdateCellParams extends IEditRowOperationParams { + columnId: number; + newValue: string; +} + +export interface EditUpdateCellResult extends EditCellResult {} + +//#endregion + +//#region edit/revertCell + +export interface EditRevertCellParams extends IEditRowOperationParams { + columnId: number; +} + +export interface EditRevertCellResult extends EditCellResult {} + +//#endregion + +//#region edit/dispose + +export interface EditDisposeParams extends IEditSessionOperationParams {} + +export interface EditDisposeResult {} + +//#endregion + +//#region edit/script + +export interface EditScriptParams extends IEditSessionOperationParams {} + +export interface EditScriptResult { + scripts: string[]; +} + +//#endregion + +export interface TableExplorerWebViewState { + tableName: string; + databaseName: string; + serverName: string; + schemaName?: string; + connectionProfile?: any; + isLoading: boolean; + ownerUri: string; + resultSet: EditSubsetResult | undefined; + currentRowCount: number; // Track the user's selected row count for data loading + newRows: EditRow[]; // Track newly created rows that haven't been committed yet + updateScript?: string; // SQL script generated from pending changes + showScriptPane: boolean; // Whether to show the script pane + currentPage?: number; // Track the current page number in the data grid + failedCells?: string[]; // Track cells that failed to update (format: "rowId-columnId") +} + +export interface TableExplorerContextProps { + commitChanges: () => void; + loadSubset: (rowCount: number) => void; + createRow: () => void; + deleteRow: (rowId: number) => void; + updateCell: (rowId: number, columnId: number, newValue: string) => void; + revertCell: (rowId: number, columnId: number) => void; + revertRow: (rowId: number) => void; + generateScript: () => void; + openScriptInEditor: () => void; + copyScriptToClipboard: () => void; + toggleScriptPane: () => void; + setCurrentPage: (pageNumber: number) => void; +} + +export interface TableExplorerReducers { + commitChanges: {}; + loadSubset: { rowCount: number }; + createRow: {}; + deleteRow: { rowId: number }; + updateCell: { rowId: number; columnId: number; newValue: string }; + revertCell: { rowId: number; columnId: number }; + revertRow: { rowId: number }; + generateScript: {}; + openScriptInEditor: {}; + copyScriptToClipboard: {}; + toggleScriptPane: {}; + setCurrentPage: { pageNumber: number }; +} diff --git a/src/sharedInterfaces/telemetry.ts b/src/sharedInterfaces/telemetry.ts index e965d46e8e..67a961277b 100644 --- a/src/sharedInterfaces/telemetry.ts +++ b/src/sharedInterfaces/telemetry.ts @@ -32,6 +32,7 @@ export enum TelemetryViews { Connection = "Connection", Credential = "Credential", ConnectionManager = "ConnectionManager", + TableExplorer = "TableExplorer", } export enum TelemetryActions { @@ -158,6 +159,13 @@ export enum TelemetryActions { GetSqlAnalyticsEndpointUrlFromFabric = "GetSqlAnalyticsEndpointUrlFromFabric", SurveyFunnel = "SurveyFunnel", CopilotNewQueryWithConnection = "CopilotNewQueryWithConnection", + CommitChanges = "CommitChanges", + CreateRow = "CreateRow", + DeleteRow = "DeleteRow", + UpdateCell = "UpdateCell", + RevertCell = "RevertCell", + RevertRow = "RevertRow", + LoadSubset = "LoadSubset", } /** diff --git a/src/tableExplorer/tableExplorerWebViewController.ts b/src/tableExplorer/tableExplorerWebViewController.ts new file mode 100644 index 0000000000..4edc415301 --- /dev/null +++ b/src/tableExplorer/tableExplorerWebViewController.ts @@ -0,0 +1,1021 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from "vscode"; +import { ReactWebviewPanelController } from "../controllers/reactWebviewPanelController"; +import { + TableExplorerWebViewState, + TableExplorerReducers, + EditSessionReadyParams, +} from "../sharedInterfaces/tableExplorer"; +import { TreeNodeInfo } from "../objectExplorer/nodes/treeNodeInfo"; +import ConnectionManager from "../controllers/connectionManager"; +import VscodeWrapper from "../controllers/vscodeWrapper"; +import { ObjectExplorerUtils } from "../objectExplorer/objectExplorerUtils"; +import { ITableExplorerService } from "../services/tableExplorerService"; +import { EditSessionReadyNotification } from "../models/contracts/tableExplorer"; +import { NotificationHandler } from "vscode-languageclient"; +import * as LocConstants from "../constants/locConstants"; +import { getErrorMessage } from "../utils/utils"; +import { sendActionEvent, startActivity } from "../telemetry/telemetry"; +import { ActivityStatus, TelemetryActions, TelemetryViews } from "../sharedInterfaces/telemetry"; +import { generateGuid } from "../models/utils"; + +export class TableExplorerWebViewController extends ReactWebviewPanelController< + TableExplorerWebViewState, + TableExplorerReducers +> { + private operationId: string; + + constructor( + context: vscode.ExtensionContext, + vscodeWrapper: VscodeWrapper, + private _tableExplorerService: ITableExplorerService, + private _connectionManager: ConnectionManager, + private _targetNode: TreeNodeInfo, + ) { + const tableName = _targetNode?.metadata?.name || "Table"; + const schemaName = _targetNode?.metadata?.schema; + const databaseName = ObjectExplorerUtils.getDatabaseName(_targetNode); + const serverName = _targetNode?.connectionProfile?.server || ""; + const qualifiedTableName = schemaName ? `${schemaName}.${tableName}` : tableName; + + super( + context, + vscodeWrapper, + "tableExplorer", + "tableExplorer", + { + tableName: tableName, + databaseName: databaseName, + serverName: serverName, + connectionProfile: _targetNode?.connectionProfile, + schemaName: schemaName, + isLoading: false, + ownerUri: "", + resultSet: undefined, + currentRowCount: 100, // Default row count for data loading + newRows: [], // Track newly created rows + updateScript: undefined, // No script initially + showScriptPane: false, // Script pane hidden by default + currentPage: 1, // Start on page 1 + failedCells: [], // Track cells that failed to update + }, + { + title: LocConstants.TableExplorer.title(qualifiedTableName), + viewColumn: vscode.ViewColumn.Active, + iconPath: { + dark: vscode.Uri.joinPath( + context.extensionUri, + "media", + "objectTypes", + "EditTableData_Dark.svg", + ), + light: vscode.Uri.joinPath( + context.extensionUri, + "media", + "objectTypes", + "EditTableData_Light.svg", + ), + }, + showRestorePromptAfterClose: false, // Will be set to true when changes are made + }, + ); + + this.operationId = generateGuid(); + this.logger.info( + `TableExplorerWebViewController created for table: ${tableName} in database: ${databaseName} - OperationId: ${this.operationId}`, + ); + + this._tableExplorerService.sqlToolsClient.onNotification( + EditSessionReadyNotification.type, + this.handleEditSessionReadyNotification(), + ); + + void this.initialize(); + this.registerRpcHandlers(); + } + + /** + * Initializes the table explorer with the given node context. + */ + private async initialize(): Promise { + const startTime = Date.now(); + const endActivity = startActivity( + TelemetryViews.TableExplorer, + TelemetryActions.Initialize, + generateGuid(), + { + startTime: startTime.toString(), + operationId: this.operationId, + }, + ); + + if (!this._targetNode) { + this.logger.error(`No target node provided - OperationId: ${this.operationId}`); + endActivity.endFailed( + new Error("No target node provided for table explorer"), + true, + undefined, + undefined, + { + elapsedTime: (Date.now() - startTime).toString(), + operationId: this.operationId, + }, + ); + + await vscode.window.showErrorMessage( + LocConstants.TableExplorer.unableToOpenTableExplorer, + ); + return; + } + + try { + const schemaName = this.state.schemaName; + const objectName = this.state.tableName; + const ownerUri = schemaName + ? `untitled:${schemaName}.${objectName}` + : `untitled:${objectName}`; + + const objectType = this._targetNode.metadata.metadataTypeName.toUpperCase(); + + let connectionCreds = Object.assign({}, this._targetNode.connectionProfile); + const databaseName = ObjectExplorerUtils.getDatabaseName(this._targetNode); + + this.logger.info( + `Initializing table explorer for ${schemaName}.${objectName} - OperationId: ${this.operationId}`, + ); + + if ( + !this._connectionManager.isConnected(ownerUri) || + connectionCreds.database !== databaseName + ) { + connectionCreds.database = databaseName; + if (!this._connectionManager.isConnecting(ownerUri)) { + await this._connectionManager.connect(ownerUri, connectionCreds); + } + } + + await this._tableExplorerService.initialize( + ownerUri, + objectName, + schemaName, + objectType, + undefined, + ); + + this.logger.info( + `Table explorer initialized successfully - OperationId: ${this.operationId}`, + ); + endActivity.end(ActivityStatus.Succeeded, { + elapsedTime: (Date.now() - startTime).toString(), + operationId: this.operationId, + }); + } catch (error) { + this.logger.error( + `Error initializing table explorer: ${getErrorMessage(error)} - OperationId: ${this.operationId}`, + ); + endActivity.endFailed( + new Error(`Failed to initialize table explorer: ${getErrorMessage(error)}`), + true, + undefined, + undefined, + { + elapsedTime: (Date.now() - startTime).toString(), + operationId: this.operationId, + }, + ); + throw error; + } + } + + private handleEditSessionReadyNotification(): NotificationHandler { + const self = this; + return (result: EditSessionReadyParams): void => { + if (result.success) { + self.state.ownerUri = result.ownerUri; + self.updateState(); + + void self.loadResultSet(); + } + }; + } + + private async loadResultSet(): Promise { + const subsetResult = await this._tableExplorerService.subset(this.state.ownerUri, 0, 100); + this.state.resultSet = subsetResult; + + this.updateState(); + } + + /** + * Helper method to regenerate the script and update state. + * Used when script pane is visible and data changes occur. + */ + private async regenerateScript(state: TableExplorerWebViewState): Promise { + try { + const scriptResult = await this._tableExplorerService.generateScripts(state.ownerUri); + const combinedScript = scriptResult.scripts?.join("\n") || ""; + state.updateScript = combinedScript; + this.updateState(); + this.logger.info("Script regenerated successfully in real-time"); + } catch (error) { + this.logger.error(`Error regenerating script: ${error}`); + } + } + + /** + * Helper method to conditionally regenerate script if script pane is visible. + * Call this after updating state when data changes occur. + */ + private async regenerateScriptIfVisible(state: TableExplorerWebViewState): Promise { + if (state.showScriptPane) { + await this.regenerateScript(state); + } + } + + private registerRpcHandlers(): void { + this.registerReducer("commitChanges", async (state) => { + this.logger.info( + `Committing changes for: ${state.tableName} - OperationId: ${this.operationId}`, + ); + + const startTime = Date.now(); + const endActivity = startActivity( + TelemetryViews.TableExplorer, + TelemetryActions.CommitChanges, + generateGuid(), + { + startTime: startTime.toString(), + operationId: this.operationId, + newRowsCount: state.newRows.length.toString(), + }, + ); + + try { + await this._tableExplorerService.commit(state.ownerUri); + vscode.window.showInformationMessage( + LocConstants.TableExplorer.changesSavedSuccessfully, + ); + + // Clear tracking state after successful commit + state.newRows = []; + state.failedCells = []; + this.showRestorePromptAfterClose = false; + + this.logger.info( + `Cleared new rows and failed cells after successful commit - OperationId: ${this.operationId}`, + ); + + endActivity.end(ActivityStatus.Succeeded, { + elapsedTime: (Date.now() - startTime).toString(), + operationId: this.operationId, + }); + } catch (error) { + this.logger.error( + `Error committing changes: ${getErrorMessage(error)} - OperationId: ${this.operationId}`, + ); + + endActivity.endFailed( + new Error(`Failed to commit changes: ${getErrorMessage(error)}`), + true, + undefined, + undefined, + { + elapsedTime: (Date.now() - startTime).toString(), + operationId: this.operationId, + }, + ); + + vscode.window.showErrorMessage( + LocConstants.TableExplorer.failedToSaveChanges(getErrorMessage(error)), + ); + } + + return state; + }); + + this.registerReducer("loadSubset", async (state, payload) => { + this.logger.info( + `Loading subset with rowCount: ${payload.rowCount} - OperationId: ${this.operationId}`, + ); + + const startTime = Date.now(); + const endActivity = startActivity( + TelemetryViews.TableExplorer, + TelemetryActions.LoadSubset, + generateGuid(), + { + startTime: startTime.toString(), + operationId: this.operationId, + rowCount: payload.rowCount.toString(), + }, + ); + + try { + const subsetResult = await this._tableExplorerService.subset( + state.ownerUri, + 0, + payload.rowCount, + ); + + // Filter out any new uncommitted rows from backend result to avoid duplicates + // We'll always append them explicitly at the end + const newRowIds = new Set(state.newRows.map((row) => row.id)); + const backendRowsOnly = subsetResult.subset.filter((row) => !newRowIds.has(row.id)); + + // Always append new rows at the end + state.resultSet = { + ...subsetResult, + subset: [...backendRowsOnly, ...state.newRows], + rowCount: backendRowsOnly.length + state.newRows.length, + }; + + this.logger.info( + `Loaded ${backendRowsOnly.length} committed rows from database, appended ${state.newRows.length} new uncommitted rows - OperationId: ${this.operationId}`, + ); + + state.currentRowCount = payload.rowCount; + + this.updateState(); + + endActivity.end(ActivityStatus.Succeeded, { + elapsedTime: (Date.now() - startTime).toString(), + operationId: this.operationId, + rowsLoaded: subsetResult.rowCount.toString(), + }); + } catch (error) { + this.logger.error( + `Error loading subset: ${getErrorMessage(error)} - OperationId: ${this.operationId}`, + ); + + endActivity.endFailed( + new Error(`Failed to load subset: ${getErrorMessage(error)}`), + true, + undefined, + undefined, + { + elapsedTime: (Date.now() - startTime).toString(), + operationId: this.operationId, + }, + ); + + vscode.window.showErrorMessage( + LocConstants.TableExplorer.failedToLoadData(getErrorMessage(error)), + ); + } + + return state; + }); + + this.registerReducer("createRow", async (state) => { + this.logger.info( + `Creating new row for: ${state.tableName} - OperationId: ${this.operationId}`, + ); + + const startTime = Date.now(); + const endActivity = startActivity( + TelemetryViews.TableExplorer, + TelemetryActions.CreateRow, + generateGuid(), + { + startTime: startTime.toString(), + operationId: this.operationId, + }, + ); + + try { + const result = await this._tableExplorerService.createRow(state.ownerUri); + vscode.window.showInformationMessage( + LocConstants.TableExplorer.rowCreatedSuccessfully, + ); + this.logger.info( + `Created row with ID: ${result.newRowId} - OperationId: ${this.operationId}`, + ); + + // Track new row and mark unsaved changes + state.newRows = [...state.newRows, result.row]; + this.showRestorePromptAfterClose = true; + + // Update result set with new row + if (state.resultSet) { + state.resultSet = { + ...state.resultSet, + subset: [...state.resultSet.subset, result.row], + rowCount: state.resultSet.rowCount + 1, + }; + + this.updateState(); + + this.logger.info( + `Added new row to result set, now has ${state.resultSet.rowCount} rows (${state.newRows.length} new)`, + ); + } else { + this.logger.warn("Cannot add row: result set is undefined"); + } + + await this.regenerateScriptIfVisible(state); + + endActivity.end(ActivityStatus.Succeeded, { + elapsedTime: (Date.now() - startTime).toString(), + operationId: this.operationId, + }); + } catch (error) { + this.logger.error( + `Error creating row: ${getErrorMessage(error)} - OperationId: ${this.operationId}`, + ); + + endActivity.endFailed( + new Error(`Failed to create row: ${getErrorMessage(error)}`), + true, + undefined, + undefined, + { + elapsedTime: (Date.now() - startTime).toString(), + operationId: this.operationId, + }, + ); + + vscode.window.showErrorMessage( + LocConstants.TableExplorer.failedToCreateNewRow(getErrorMessage(error)), + ); + } + + return state; + }); + + this.registerReducer("deleteRow", async (state, payload) => { + this.logger.info(`Deleting row: ${payload.rowId} - OperationId: ${this.operationId}`); + + const startTime = Date.now(); + const endActivity = startActivity( + TelemetryViews.TableExplorer, + TelemetryActions.DeleteRow, + generateGuid(), + { + startTime: startTime.toString(), + operationId: this.operationId, + }, + ); + + try { + await this._tableExplorerService.deleteRow(state.ownerUri, payload.rowId); + vscode.window.showInformationMessage(LocConstants.TableExplorer.rowRemoved); + + // Remove from newRows tracking if it was a new row + state.newRows = state.newRows.filter((row) => row.id !== payload.rowId); + + // Remove all failed cells for this row + if (state.failedCells) { + state.failedCells = state.failedCells.filter( + (key) => !key.startsWith(`${payload.rowId}-`), + ); + } + + this.showRestorePromptAfterClose = true; + + // Update result set + if (state.resultSet) { + const updatedSubset = state.resultSet.subset.filter( + (row) => row.id !== payload.rowId, + ); + state.resultSet = { + ...state.resultSet, + subset: updatedSubset, + rowCount: updatedSubset.length, + }; + + this.updateState(); + + this.logger.info( + `Updated result set, now has ${updatedSubset.length} rows (${state.newRows.length} new)`, + ); + } + + await this.regenerateScriptIfVisible(state); + + endActivity.end(ActivityStatus.Succeeded, { + elapsedTime: (Date.now() - startTime).toString(), + operationId: this.operationId, + }); + } catch (error) { + this.logger.error( + `Error deleting row: ${getErrorMessage(error)} - OperationId: ${this.operationId}`, + ); + + endActivity.endFailed( + new Error(`Failed to delete row: ${getErrorMessage(error)}`), + true, + undefined, + undefined, + { + elapsedTime: (Date.now() - startTime).toString(), + operationId: this.operationId, + }, + ); + + vscode.window.showErrorMessage( + LocConstants.TableExplorer.failedToRemoveRow(getErrorMessage(error)), + ); + } + + return state; + }); + + this.registerReducer("updateCell", async (state, payload) => { + this.logger.info( + `Updating cell: row ${payload.rowId}, column ${payload.columnId} - OperationId: ${this.operationId}`, + ); + + const startTime = Date.now(); + const endActivity = startActivity( + TelemetryViews.TableExplorer, + TelemetryActions.UpdateCell, + generateGuid(), + { + startTime: startTime.toString(), + operationId: this.operationId, + }, + ); + + try { + const updateCellResult = await this._tableExplorerService.updateCell( + state.ownerUri, + payload.rowId, + payload.columnId, + payload.newValue, + ); + + this.showRestorePromptAfterClose = true; + + // Update the cell value in the result set + if (state.resultSet && updateCellResult.cell) { + const rowIndex = state.resultSet.subset.findIndex( + (row) => row.id === payload.rowId, + ); + + if (rowIndex !== -1) { + state.resultSet.subset[rowIndex].cells[payload.columnId] = + updateCellResult.cell; + + this.updateState(); + + this.logger.info( + `Updated cell in result set at row ${rowIndex}, column ${payload.columnId}`, + ); + } + } + + this.logger.info(`Cell updated successfully - OperationId: ${this.operationId}`); + + await this.regenerateScriptIfVisible(state); + + endActivity.end(ActivityStatus.Succeeded, { + elapsedTime: (Date.now() - startTime).toString(), + operationId: this.operationId, + }); + } catch (error) { + this.logger.error( + `Error updating cell: ${getErrorMessage(error)} - OperationId: ${this.operationId}`, + ); + + // Track failed cell for UI highlighting + const failedKey = `${payload.rowId}-${payload.columnId}`; + if (!state.failedCells) { + state.failedCells = [failedKey]; + } else if (!state.failedCells.includes(failedKey)) { + state.failedCells = [...state.failedCells, failedKey]; + } + + this.updateState(); + + endActivity.endFailed( + new Error(`Failed to update cell: ${getErrorMessage(error)}`), + true, + undefined, + undefined, + { + elapsedTime: (Date.now() - startTime).toString(), + operationId: this.operationId, + }, + ); + + vscode.window.showErrorMessage( + LocConstants.TableExplorer.failedToUpdateCell(getErrorMessage(error)), + ); + } + + return state; + }); + + this.registerReducer("revertCell", async (state, payload) => { + this.logger.info( + `Reverting cell: row ${payload.rowId}, column ${payload.columnId} - OperationId: ${this.operationId}`, + ); + + const startTime = Date.now(); + const endActivity = startActivity( + TelemetryViews.TableExplorer, + TelemetryActions.RevertCell, + generateGuid(), + { + startTime: startTime.toString(), + operationId: this.operationId, + }, + ); + + try { + const revertCellResult = await this._tableExplorerService.revertCell( + state.ownerUri, + payload.rowId, + payload.columnId, + ); + + // Remove from failed cells tracking + if (state.failedCells) { + const failedKey = `${payload.rowId}-${payload.columnId}`; + state.failedCells = state.failedCells.filter((key) => key !== failedKey); + } + + // Update the cell value in the result set + if (state.resultSet && revertCellResult.cell) { + const rowIndex = state.resultSet.subset.findIndex( + (row) => row.id === payload.rowId, + ); + + if (rowIndex !== -1) { + state.resultSet = { + ...state.resultSet, + subset: state.resultSet.subset.map((row, idx) => { + if (idx === rowIndex) { + return { + ...row, + cells: row.cells.map((cell, cellIdx) => { + if (cellIdx === payload.columnId) { + return revertCellResult.cell; + } + return cell; + }), + }; + } + return row; + }), + }; + + this.updateState(); + + this.logger.info( + `Reverted cell in result set at row ${rowIndex}, column ${payload.columnId}`, + ); + } + } + + this.logger.info(`Cell reverted successfully - OperationId: ${this.operationId}`); + + await this.regenerateScriptIfVisible(state); + + endActivity.end(ActivityStatus.Succeeded, { + elapsedTime: (Date.now() - startTime).toString(), + operationId: this.operationId, + }); + } catch (error) { + this.logger.error( + `Error reverting cell: ${getErrorMessage(error)} - OperationId: ${this.operationId}`, + ); + + endActivity.endFailed( + new Error(`Failed to revert cell: ${getErrorMessage(error)}`), + true, + undefined, + undefined, + { + elapsedTime: (Date.now() - startTime).toString(), + operationId: this.operationId, + }, + ); + + vscode.window.showErrorMessage( + LocConstants.TableExplorer.failedToRevertCell(getErrorMessage(error)), + ); + } + + return state; + }); + + this.registerReducer("revertRow", async (state, payload) => { + this.logger.info(`Reverting row: ${payload.rowId} - OperationId: ${this.operationId}`); + + const startTime = Date.now(); + const endActivity = startActivity( + TelemetryViews.TableExplorer, + TelemetryActions.RevertRow, + generateGuid(), + { + startTime: startTime.toString(), + operationId: this.operationId, + }, + ); + + try { + const revertRowResult = await this._tableExplorerService.revertRow( + state.ownerUri, + payload.rowId, + ); + + // Remove all failed cells for this row + if (state.failedCells) { + state.failedCells = state.failedCells.filter( + (key) => !key.startsWith(`${payload.rowId}-`), + ); + } + + // Update the row in the result set + if (state.resultSet && revertRowResult.row) { + const rowIndex = state.resultSet.subset.findIndex( + (row) => row.id === payload.rowId, + ); + + if (rowIndex !== -1) { + state.resultSet = { + ...state.resultSet, + subset: state.resultSet.subset.map((row, idx) => { + if (idx === rowIndex) { + return revertRowResult.row; + } + + return row; + }), + }; + + this.updateState(); + + this.logger.info( + `Reverted row at index ${rowIndex} with ${revertRowResult.row.cells.length} cells`, + ); + } + } + + this.logger.info(`Row reverted successfully - OperationId: ${this.operationId}`); + + await this.regenerateScriptIfVisible(state); + + endActivity.end(ActivityStatus.Succeeded, { + elapsedTime: (Date.now() - startTime).toString(), + operationId: this.operationId, + }); + } catch (error) { + this.logger.error( + `Error reverting row: ${getErrorMessage(error)} - OperationId: ${this.operationId}`, + ); + + endActivity.endFailed( + new Error(`Failed to revert row: ${getErrorMessage(error)}`), + true, + undefined, + undefined, + { + elapsedTime: (Date.now() - startTime).toString(), + operationId: this.operationId, + }, + ); + + vscode.window.showErrorMessage( + LocConstants.TableExplorer.failedToRevertRow(getErrorMessage(error)), + ); + } + + return state; + }); + + this.registerReducer("generateScript", async (state) => { + this.logger.info( + `Generating update script for: ${state.tableName} - OperationId: ${this.operationId}`, + ); + + const startTime = Date.now(); + const endActivity = startActivity( + TelemetryViews.TableExplorer, + TelemetryActions.GenerateScript, + generateGuid(), + { + startTime: startTime.toString(), + operationId: this.operationId, + }, + ); + + try { + const scriptResult = await this._tableExplorerService.generateScripts( + state.ownerUri, + ); + + // Combine script array into single string + const combinedScript = scriptResult.scripts?.join("\n") || ""; + this.logger.info( + `Script result received: ${scriptResult.scripts?.length} script(s), combined length: ${combinedScript.length} - OperationId: ${this.operationId}`, + ); + + // Update state with script and show pane + state.updateScript = combinedScript; + state.showScriptPane = true; + + this.logger.info( + `State before updateState - updateScript length: ${state.updateScript?.length}, showScriptPane: ${state.showScriptPane}`, + ); + this.updateState(); + this.logger.info( + `State after updateState - this.state.updateScript length: ${this.state.updateScript?.length} - OperationId: ${this.operationId}`, + ); + + this.logger.info( + `Script generated successfully - OperationId: ${this.operationId}`, + ); + + endActivity.end(ActivityStatus.Succeeded, { + elapsedTime: (Date.now() - startTime).toString(), + operationId: this.operationId, + scriptCount: scriptResult.scripts?.length.toString() || "0", + }); + } catch (error) { + this.logger.error( + `Error generating script: ${getErrorMessage(error)} - OperationId: ${this.operationId}`, + ); + + endActivity.endFailed( + new Error(`Failed to generate script: ${getErrorMessage(error)}`), + true, + undefined, + undefined, + { + elapsedTime: (Date.now() - startTime).toString(), + operationId: this.operationId, + }, + ); + + vscode.window.showErrorMessage( + LocConstants.TableExplorer.failedToGenerateScript(getErrorMessage(error)), + ); + } + + return state; + }); + + this.registerReducer("openScriptInEditor", async (state) => { + this.logger.info(`Opening script in SQL editor - OperationId: ${this.operationId}`); + + sendActionEvent(TelemetryViews.TableExplorer, TelemetryActions.Open, { + operationId: this.operationId, + context: "scriptEditor", + }); + + try { + if (state.updateScript) { + const doc = await vscode.workspace.openTextDocument({ + content: state.updateScript, + language: "sql", + }); + await vscode.window.showTextDocument(doc); + + this.logger.info( + `Script opened in SQL editor successfully - OperationId: ${this.operationId}`, + ); + } else { + vscode.window.showWarningMessage(LocConstants.TableExplorer.noScriptToOpen); + } + } catch (error) { + this.logger.error( + `Error opening script in editor: ${getErrorMessage(error)} - OperationId: ${this.operationId}`, + ); + vscode.window.showErrorMessage( + LocConstants.TableExplorer.failedToOpenScript(getErrorMessage(error)), + ); + } + + return state; + }); + + this.registerReducer("copyScriptToClipboard", async (state) => { + this.logger.info(`Copying script to clipboard - OperationId: ${this.operationId}`); + + sendActionEvent(TelemetryViews.TableExplorer, TelemetryActions.CopyResults, { + operationId: this.operationId, + context: "script", + }); + + try { + if (state.updateScript) { + await vscode.env.clipboard.writeText(state.updateScript); + await vscode.window.showInformationMessage( + LocConstants.TableExplorer.scriptCopiedToClipboard, + ); + + this.logger.info( + `Script copied to clipboard successfully - OperationId: ${this.operationId}`, + ); + } else { + vscode.window.showWarningMessage(LocConstants.TableExplorer.noScriptToCopy); + } + } catch (error) { + this.logger.error( + `Error copying script to clipboard: ${getErrorMessage(error)} - OperationId: ${this.operationId}`, + ); + vscode.window.showErrorMessage( + LocConstants.TableExplorer.failedToCopyScript(getErrorMessage(error)), + ); + } + + return state; + }); + + this.registerReducer("toggleScriptPane", async (state) => { + state.showScriptPane = !state.showScriptPane; + + this.logger.info( + `Script pane toggled to: ${state.showScriptPane} - OperationId: ${this.operationId}`, + ); + + sendActionEvent(TelemetryViews.TableExplorer, TelemetryActions.Close, { + operationId: this.operationId, + context: "scriptPane", + action: state.showScriptPane ? "opened" : "closed", + }); + this.updateState(); + + return state; + }); + + this.registerReducer("setCurrentPage", async (state, payload) => { + state.currentPage = payload.pageNumber; + + this.logger.info(`Current page set to: ${payload.pageNumber}`); + + return state; + }); + } + + /** + * Override the base class's showRestorePrompt to handle unsaved changes. + * This is called from the onDidDispose handler in the base class. + * Prompts the user to save or discard changes, then allows disposal to continue. + * Always returns undefined to allow the close to proceed after handling the user's choice. + */ + protected override async showRestorePrompt(): Promise<{ + title: string; + run: () => Promise; + }> { + const result = await vscode.window.showWarningMessage( + LocConstants.TableExplorer.unsavedChangesPrompt(this.state.tableName), + { + modal: true, + }, + LocConstants.TableExplorer.Save, + LocConstants.TableExplorer.Discard, + ); + + // Handle the user's choice + if (result === LocConstants.TableExplorer.Save) { + this.logger.info("User chose to save changes before closing"); + + try { + await this._tableExplorerService.commit(this.state.ownerUri); + vscode.window.showInformationMessage( + LocConstants.TableExplorer.changesSavedSuccessfully, + ); + + this.logger.info("Changes saved successfully before closing"); + } catch (error) { + this.logger.error(`Error saving changes before closing: ${error}`); + vscode.window.showErrorMessage( + LocConstants.TableExplorer.failedToSaveChanges(getErrorMessage(error)), + ); + } + } else if (result === LocConstants.TableExplorer.Discard) { + this.logger.info("User chose to discard changes"); + } else { + this.logger.info("User dismissed the prompt - treating as discard"); + } + + // Always return undefined to allow disposal to continue + return undefined; + } + + /** + * Disposes the Table Explorer webview controller and cleans up resources. + * This is called when the webview tab is closed (after any prompts are handled). + */ + public override dispose(): void { + if (this.state.ownerUri) { + this.logger.info( + `Disposing Table Explorer resources for ownerUri: ${this.state.ownerUri}`, + ); + void this._tableExplorerService.dispose(this.state.ownerUri).catch((error) => { + this.logger.error( + `Error disposing table explorer service: ${getErrorMessage(error)}`, + ); + }); + } + + super.dispose(); + } +} diff --git a/test/unit/tableExplorerService.test.ts b/test/unit/tableExplorerService.test.ts new file mode 100644 index 0000000000..4dcde20a26 --- /dev/null +++ b/test/unit/tableExplorerService.test.ts @@ -0,0 +1,839 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as sinon from "sinon"; +import { expect } from "chai"; +import { TableExplorerService } from "../../src/services/tableExplorerService"; +import SqlToolsServiceClient from "../../src/languageservice/serviceclient"; +import { + EditCommitRequest, + EditCreateRowRequest, + EditDeleteRowRequest, + EditDisposeRequest, + EditInitializeRequest, + EditRevertCellRequest, + EditRevertRowRequest, + EditScriptRequest, + EditSubsetRequest, + EditUpdateCellRequest, +} from "../../src/models/contracts/tableExplorer"; +import { + EditCommitResult, + EditCreateRowResult, + EditDeleteRowResult, + EditDisposeResult, + EditInitializeResult, + EditRevertCellResult, + EditRevertRowResult, + EditRowState, + EditScriptResult, + EditSubsetResult, + EditUpdateCellResult, +} from "../../src/sharedInterfaces/tableExplorer"; +import { Logger } from "../../src/models/logger"; + +suite("TableExplorerService Tests", () => { + let sandbox: sinon.SinonSandbox; + let mockClient: sinon.SinonStubbedInstance; + let mockLogger: sinon.SinonStubbedInstance; + let tableExplorerService: TableExplorerService; + + setup(() => { + sandbox = sinon.createSandbox(); + mockClient = sandbox.createStubInstance(SqlToolsServiceClient); + mockLogger = sandbox.createStubInstance(Logger); + + sandbox.stub(mockClient, "logger").get(() => mockLogger); + + tableExplorerService = new TableExplorerService(mockClient); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite("constructor and properties", () => { + test("should initialize with SqlToolsServiceClient", () => { + expect(tableExplorerService).to.not.be.undefined; + expect(tableExplorerService.sqlToolsClient).to.equal(mockClient); + }); + + test("sqlToolsClient getter should return the client instance", () => { + const client = tableExplorerService.sqlToolsClient; + expect(client).to.equal(mockClient); + }); + }); + + suite("initialize", () => { + const ownerUri = "test-owner-uri"; + const objectName = "TestTable"; + const schemaName = "dbo"; + const objectType = "Table"; + const queryString = "SELECT * FROM TestTable"; + const limitResults = 100; + + test("should successfully initialize with all parameters", async () => { + const mockResult: EditInitializeResult = {}; + + mockClient.sendRequest + .withArgs(EditInitializeRequest.type, sinon.match.any) + .resolves(mockResult); + + const result = await tableExplorerService.initialize( + ownerUri, + objectName, + schemaName, + objectType, + queryString, + limitResults, + ); + + expect(result).to.equal(mockResult); + expect(mockClient.sendRequest.calledOnce).to.be.true; + + const callArgs = mockClient.sendRequest.firstCall.args; + expect(callArgs[0]).to.equal(EditInitializeRequest.type); + expect(callArgs[1]).to.deep.equal({ + ownerUri: ownerUri, + filters: { LimitResults: limitResults }, + objectName: objectName, + schemaName: schemaName, + objectType: objectType, + queryString: queryString, + }); + }); + + test("should initialize without limit results", async () => { + const mockResult: EditInitializeResult = {}; + + mockClient.sendRequest + .withArgs(EditInitializeRequest.type, sinon.match.any) + .resolves(mockResult); + + const result = await tableExplorerService.initialize( + ownerUri, + objectName, + schemaName, + objectType, + queryString, + ); + + expect(result).to.equal(mockResult); + + const callArgs = mockClient.sendRequest.firstCall.args; + expect((callArgs[1] as any).filters.LimitResults).to.be.undefined; + }); + + test("should initialize without query string", async () => { + const mockResult: EditInitializeResult = {}; + + mockClient.sendRequest + .withArgs(EditInitializeRequest.type, sinon.match.any) + .resolves(mockResult); + + const result = await tableExplorerService.initialize( + ownerUri, + objectName, + schemaName, + objectType, + undefined, + limitResults, + ); + + expect(result).to.equal(mockResult); + + const callArgs = mockClient.sendRequest.firstCall.args; + expect((callArgs[1] as any).queryString).to.be.undefined; + }); + + test("should handle initialization error and log it", async () => { + const error = new Error("Initialization failed"); + mockClient.sendRequest + .withArgs(EditInitializeRequest.type, sinon.match.any) + .rejects(error); + + try { + await tableExplorerService.initialize( + ownerUri, + objectName, + schemaName, + objectType, + queryString, + limitResults, + ); + expect.fail("Should have thrown an error"); + } catch (err) { + expect(err).to.equal(error); + expect(mockLogger.error.calledOnce).to.be.true; + expect(mockLogger.error.firstCall.args[0]).to.equal("Initialization failed"); + } + }); + }); + + suite("subset", () => { + const ownerUri = "test-owner-uri"; + const rowStartIndex = 0; + const rowCount = 50; + + test("should successfully retrieve subset of rows", async () => { + const mockResult: EditSubsetResult = { + rowCount: 50, + subset: [ + { + cells: [ + { displayValue: "1", isNull: false, invariantCultureDisplayValue: "1" }, + { + displayValue: "Test", + isNull: false, + invariantCultureDisplayValue: "Test", + }, + ], + id: 0, + isDirty: false, + state: EditRowState.clean, + }, + ], + columnInfo: [ + { name: "Id", isEditable: true }, + { name: "Name", isEditable: true }, + ], + }; + + mockClient.sendRequest + .withArgs(EditSubsetRequest.type, sinon.match.any) + .resolves(mockResult); + + const result = await tableExplorerService.subset(ownerUri, rowStartIndex, rowCount); + + expect(result).to.equal(mockResult); + expect(mockClient.sendRequest.calledOnce).to.be.true; + + const callArgs = mockClient.sendRequest.firstCall.args; + expect(callArgs[0]).to.equal(EditSubsetRequest.type); + expect(callArgs[1]).to.deep.equal({ + ownerUri: ownerUri, + rowStartIndex: rowStartIndex, + rowCount: rowCount, + }); + }); + + test("should handle subset request with different row indices", async () => { + const mockResult: EditSubsetResult = { + rowCount: 25, + subset: [], + columnInfo: [], + }; + + mockClient.sendRequest + .withArgs(EditSubsetRequest.type, sinon.match.any) + .resolves(mockResult); + + const result = await tableExplorerService.subset(ownerUri, 100, 25); + + expect(result).to.equal(mockResult); + + await tableExplorerService.subset(ownerUri, 100, 25); + + const callArgs = mockClient.sendRequest.firstCall.args; + expect((callArgs[1] as any).rowStartIndex).to.equal(100); + expect((callArgs[1] as any).rowCount).to.equal(25); + }); + + test("should handle subset error and log it", async () => { + const error = new Error("Subset request failed"); + mockClient.sendRequest.withArgs(EditSubsetRequest.type, sinon.match.any).rejects(error); + + try { + await tableExplorerService.subset(ownerUri, rowStartIndex, rowCount); + expect.fail("Should have thrown an error"); + } catch (err) { + expect(err).to.equal(error); + expect(mockLogger.error.calledOnce).to.be.true; + expect(mockLogger.error.firstCall.args[0]).to.equal("Subset request failed"); + } + }); + }); + + suite("commit", () => { + const ownerUri = "test-owner-uri"; + + test("should successfully commit changes", async () => { + const mockResult: EditCommitResult = {}; + + mockClient.sendRequest + .withArgs(EditCommitRequest.type, sinon.match.any) + .resolves(mockResult); + + const result = await tableExplorerService.commit(ownerUri); + + expect(result).to.equal(mockResult); + expect(mockClient.sendRequest.calledOnce).to.be.true; + + const callArgs = mockClient.sendRequest.firstCall.args; + expect(callArgs[0]).to.equal(EditCommitRequest.type); + expect(callArgs[1]).to.deep.equal({ + ownerUri: ownerUri, + }); + }); + + test("should handle commit error and log it", async () => { + const error = new Error("Commit failed"); + mockClient.sendRequest.withArgs(EditCommitRequest.type, sinon.match.any).rejects(error); + + try { + await tableExplorerService.commit(ownerUri); + expect.fail("Should have thrown an error"); + } catch (err) { + expect(err).to.equal(error); + expect(mockLogger.error.calledOnce).to.be.true; + expect(mockLogger.error.firstCall.args[0]).to.equal("Commit failed"); + } + }); + }); + + suite("createRow", () => { + const ownerUri = "test-owner-uri"; + + test("should successfully create a new row", async () => { + const mockResult: EditCreateRowResult = { + defaultValues: ["NULL", "Default Value"], + newRowId: 42, + row: { + cells: [ + { + displayValue: "NULL", + isNull: true, + invariantCultureDisplayValue: "NULL", + }, + { + displayValue: "Default Value", + isNull: false, + invariantCultureDisplayValue: "Default Value", + }, + ], + id: 42, + isDirty: true, + state: EditRowState.dirtyInsert, + }, + }; + + mockClient.sendRequest + .withArgs(EditCreateRowRequest.type, sinon.match.any) + .resolves(mockResult); + + const result = await tableExplorerService.createRow(ownerUri); + + expect(result).to.equal(mockResult); + expect(mockClient.sendRequest.calledOnce).to.be.true; + + const callArgs = mockClient.sendRequest.firstCall.args; + expect(callArgs[0]).to.equal(EditCreateRowRequest.type); + expect(callArgs[1]).to.deep.equal({ + ownerUri: ownerUri, + }); + }); + + test("should handle createRow error and log it", async () => { + const error = new Error("Create row failed"); + mockClient.sendRequest + .withArgs(EditCreateRowRequest.type, sinon.match.any) + .rejects(error); + + try { + await tableExplorerService.createRow(ownerUri); + expect.fail("Should have thrown an error"); + } catch (err) { + expect(err).to.equal(error); + expect(mockLogger.error.calledOnce).to.be.true; + expect(mockLogger.error.firstCall.args[0]).to.equal("Create row failed"); + } + }); + }); + + suite("deleteRow", () => { + const ownerUri = "test-owner-uri"; + const rowId = 5; + + test("should successfully delete a row", async () => { + const mockResult: EditDeleteRowResult = {}; + + mockClient.sendRequest + .withArgs(EditDeleteRowRequest.type, sinon.match.any) + .resolves(mockResult); + + const result = await tableExplorerService.deleteRow(ownerUri, rowId); + + expect(result).to.equal(mockResult); + expect(mockClient.sendRequest.calledOnce).to.be.true; + + const callArgs = mockClient.sendRequest.firstCall.args; + expect(callArgs[0]).to.equal(EditDeleteRowRequest.type); + expect(callArgs[1]).to.deep.equal({ + ownerUri: ownerUri, + rowId: rowId, + }); + }); + + test("should handle deleteRow with different row IDs", async () => { + const mockResult: EditDeleteRowResult = {}; + mockClient.sendRequest + .withArgs(EditDeleteRowRequest.type, sinon.match.any) + .resolves(mockResult); + + await tableExplorerService.deleteRow(ownerUri, 999); + + const callArgs = mockClient.sendRequest.firstCall.args; + expect((callArgs[1] as any).rowId).to.equal(999); + }); + + test("should handle deleteRow error and log it", async () => { + const error = new Error("Delete row failed"); + mockClient.sendRequest + .withArgs(EditDeleteRowRequest.type, sinon.match.any) + .rejects(error); + + try { + await tableExplorerService.deleteRow(ownerUri, rowId); + expect.fail("Should have thrown an error"); + } catch (err) { + expect(err).to.equal(error); + expect(mockLogger.error.calledOnce).to.be.true; + expect(mockLogger.error.firstCall.args[0]).to.equal("Delete row failed"); + } + }); + }); + + suite("revertRow", () => { + const ownerUri = "test-owner-uri"; + const rowId = 3; + + test("should successfully revert a row", async () => { + const mockResult: EditRevertRowResult = { + row: { + cells: [ + { + displayValue: "Original", + isNull: false, + invariantCultureDisplayValue: "Original", + }, + ], + id: rowId, + isDirty: false, + state: EditRowState.clean, + }, + }; + + mockClient.sendRequest + .withArgs(EditRevertRowRequest.type, sinon.match.any) + .resolves(mockResult); + + const result = await tableExplorerService.revertRow(ownerUri, rowId); + + expect(result).to.equal(mockResult); + expect(mockClient.sendRequest.calledOnce).to.be.true; + + const callArgs = mockClient.sendRequest.firstCall.args; + expect(callArgs[0]).to.equal(EditRevertRowRequest.type); + expect(callArgs[1]).to.deep.equal({ + ownerUri: ownerUri, + rowId: rowId, + }); + }); + + test("should handle revertRow error and log it", async () => { + const error = new Error("Revert row failed"); + mockClient.sendRequest + .withArgs(EditRevertRowRequest.type, sinon.match.any) + .rejects(error); + + try { + await tableExplorerService.revertRow(ownerUri, rowId); + expect.fail("Should have thrown an error"); + } catch (err) { + expect(err).to.equal(error); + expect(mockLogger.error.calledOnce).to.be.true; + expect(mockLogger.error.firstCall.args[0]).to.equal("Revert row failed"); + } + }); + }); + + suite("updateCell", () => { + const ownerUri = "test-owner-uri"; + const rowId = 2; + const columnId = 1; + const newValue = "Updated Value"; + + test("should successfully update a cell", async () => { + const mockResult: EditUpdateCellResult = { + cell: { + displayValue: newValue, + isNull: false, + invariantCultureDisplayValue: newValue, + isDirty: true, + }, + isRowDirty: true, + }; + + mockClient.sendRequest + .withArgs(EditUpdateCellRequest.type, sinon.match.any) + .resolves(mockResult); + + const result = await tableExplorerService.updateCell( + ownerUri, + rowId, + columnId, + newValue, + ); + + expect(result).to.equal(mockResult); + expect(mockClient.sendRequest.calledOnce).to.be.true; + + const callArgs = mockClient.sendRequest.firstCall.args; + expect(callArgs[0]).to.equal(EditUpdateCellRequest.type); + expect(callArgs[1]).to.deep.equal({ + ownerUri: ownerUri, + rowId: rowId, + columnId: columnId, + newValue: newValue, + }); + }); + + test("should handle updateCell with empty string value", async () => { + const mockResult: EditUpdateCellResult = { + cell: { + displayValue: "", + isNull: false, + invariantCultureDisplayValue: "", + isDirty: true, + }, + isRowDirty: true, + }; + + mockClient.sendRequest + .withArgs(EditUpdateCellRequest.type, sinon.match.any) + .resolves(mockResult); + + const result = await tableExplorerService.updateCell(ownerUri, rowId, columnId, ""); + + expect(result).to.equal(mockResult); + + const callArgs = mockClient.sendRequest.firstCall.args; + expect((callArgs[1] as any).newValue).to.equal(""); + }); + + test("should handle updateCell error and log it", async () => { + const error = new Error("Update cell failed"); + mockClient.sendRequest + .withArgs(EditUpdateCellRequest.type, sinon.match.any) + .rejects(error); + + try { + await tableExplorerService.updateCell(ownerUri, rowId, columnId, newValue); + expect.fail("Should have thrown an error"); + } catch (err) { + expect(err).to.equal(error); + expect(mockLogger.error.calledOnce).to.be.true; + expect(mockLogger.error.firstCall.args[0]).to.equal("Update cell failed"); + } + }); + }); + + suite("revertCell", () => { + const ownerUri = "test-owner-uri"; + const rowId = 4; + const columnId = 2; + + test("should successfully revert a cell", async () => { + const mockResult: EditRevertCellResult = { + cell: { + displayValue: "Original Value", + isNull: false, + invariantCultureDisplayValue: "Original Value", + isDirty: false, + }, + isRowDirty: false, + }; + + mockClient.sendRequest + .withArgs(EditRevertCellRequest.type, sinon.match.any) + .resolves(mockResult); + + const result = await tableExplorerService.revertCell(ownerUri, rowId, columnId); + + expect(result).to.equal(mockResult); + expect(mockClient.sendRequest.calledOnce).to.be.true; + + const callArgs = mockClient.sendRequest.firstCall.args; + expect(callArgs[0]).to.equal(EditRevertCellRequest.type); + expect(callArgs[1]).to.deep.equal({ + ownerUri: ownerUri, + rowId: rowId, + columnId: columnId, + }); + }); + + test("should handle revertCell with different row and column IDs", async () => { + const mockResult: EditRevertCellResult = { + cell: { + displayValue: "Value", + isNull: false, + invariantCultureDisplayValue: "Value", + isDirty: false, + }, + isRowDirty: true, + }; + + mockClient.sendRequest + .withArgs(EditRevertCellRequest.type, sinon.match.any) + .resolves(mockResult); + + await tableExplorerService.revertCell(ownerUri, 10, 5); + + const callArgs = mockClient.sendRequest.firstCall.args; + expect((callArgs[1] as any).rowId).to.equal(10); + expect((callArgs[1] as any).columnId).to.equal(5); + }); + + test("should handle revertCell error and log it", async () => { + const error = new Error("Revert cell failed"); + mockClient.sendRequest + .withArgs(EditRevertCellRequest.type, sinon.match.any) + .rejects(error); + + try { + await tableExplorerService.revertCell(ownerUri, rowId, columnId); + expect.fail("Should have thrown an error"); + } catch (err) { + expect(err).to.equal(error); + expect(mockLogger.error.calledOnce).to.be.true; + expect(mockLogger.error.firstCall.args[0]).to.equal("Revert cell failed"); + } + }); + }); + + suite("dispose", () => { + const ownerUri = "test-owner-uri"; + + test("should successfully dispose resources", async () => { + const mockResult: EditDisposeResult = {}; + + mockClient.sendRequest + .withArgs(EditDisposeRequest.type, sinon.match.any) + .resolves(mockResult); + + const result = await tableExplorerService.dispose(ownerUri); + + expect(result).to.equal(mockResult); + expect(mockClient.sendRequest.calledOnce).to.be.true; + + const callArgs = mockClient.sendRequest.firstCall.args; + expect(callArgs[0]).to.equal(EditDisposeRequest.type); + expect(callArgs[1]).to.deep.equal({ + ownerUri: ownerUri, + }); + }); + + test("should handle dispose error and log it", async () => { + const error = new Error("Dispose failed"); + mockClient.sendRequest + .withArgs(EditDisposeRequest.type, sinon.match.any) + .rejects(error); + + try { + await tableExplorerService.dispose(ownerUri); + expect.fail("Should have thrown an error"); + } catch (err) { + expect(err).to.equal(error); + expect(mockLogger.error.calledOnce).to.be.true; + expect(mockLogger.error.firstCall.args[0]).to.equal("Dispose failed"); + } + }); + }); + + suite("generateScripts", () => { + const ownerUri = "test-owner-uri"; + + test("should successfully generate scripts", async () => { + const mockResult: EditScriptResult = { + scripts: ["UPDATE TestTable SET Name = 'Updated' WHERE Id = 1;"], + }; + + mockClient.sendRequest + .withArgs(EditScriptRequest.type, sinon.match.any) + .resolves(mockResult); + + const result = await tableExplorerService.generateScripts(ownerUri); + + expect(result).to.equal(mockResult); + expect(mockClient.sendRequest.calledOnce).to.be.true; + + const callArgs = mockClient.sendRequest.firstCall.args; + expect(callArgs[0]).to.equal(EditScriptRequest.type); + expect(callArgs[1]).to.deep.equal({ + ownerUri: ownerUri, + }); + }); + + test("should return empty script when no changes exist", async () => { + const mockResult: EditScriptResult = { + scripts: [], + }; + + mockClient.sendRequest + .withArgs(EditScriptRequest.type, sinon.match.any) + .resolves(mockResult); + + const result = await tableExplorerService.generateScripts(ownerUri); + + expect(result.scripts).to.deep.equal([]); + }); + + test("should handle generateScripts error and log it", async () => { + const error = new Error("Generate scripts failed"); + mockClient.sendRequest.withArgs(EditScriptRequest.type, sinon.match.any).rejects(error); + + try { + await tableExplorerService.generateScripts(ownerUri); + expect.fail("Should have thrown an error"); + } catch (err) { + expect(err).to.equal(error); + expect(mockLogger.error.calledOnce).to.be.true; + expect(mockLogger.error.firstCall.args[0]).to.equal("Generate scripts failed"); + } + }); + }); + + suite("error handling", () => { + test("should log error with proper message format", async () => { + const errorMessage = "Connection timeout"; + const error = new Error(errorMessage); + mockClient.sendRequest.rejects(error); + + try { + await tableExplorerService.initialize("uri", "table", "schema", "type", undefined); + expect.fail("Should have thrown an error"); + } catch (err) { + expect(mockLogger.error.calledOnce).to.be.true; + expect(mockLogger.error.firstCall.args[0]).to.contain(errorMessage); + } + }); + + test("should handle non-Error objects thrown", async () => { + const errorString = "String error"; + mockClient.sendRequest.rejects(errorString); + + try { + await tableExplorerService.commit("uri"); + expect.fail("Should have thrown an error"); + } catch (err) { + expect(mockLogger.error.calledOnce).to.be.true; + } + }); + }); + + suite("integration scenarios", () => { + test("should handle complete edit session workflow", async () => { + const ownerUri = "session-uri"; + + // Initialize + const initResult: EditInitializeResult = {}; + mockClient.sendRequest + .withArgs(EditInitializeRequest.type, sinon.match.any) + .resolves(initResult); + await tableExplorerService.initialize(ownerUri, "Table", "dbo", "Table", undefined); + + // Load subset + const subsetResult: EditSubsetResult = { + rowCount: 1, + subset: [], + columnInfo: [ + { name: "Id", isEditable: true }, + { name: "Name", isEditable: true }, + ], + }; + mockClient.sendRequest + .withArgs(EditSubsetRequest.type, sinon.match.any) + .resolves(subsetResult); + await tableExplorerService.subset(ownerUri, 0, 50); + + // Update cell + const updateResult: EditUpdateCellResult = { + cell: { + displayValue: "New", + isNull: false, + invariantCultureDisplayValue: "New", + isDirty: true, + }, + isRowDirty: true, + }; + mockClient.sendRequest + .withArgs(EditUpdateCellRequest.type, sinon.match.any) + .resolves(updateResult); + await tableExplorerService.updateCell(ownerUri, 0, 1, "New"); + + // Commit + const commitResult: EditCommitResult = {}; + mockClient.sendRequest + .withArgs(EditCommitRequest.type, sinon.match.any) + .resolves(commitResult); + await tableExplorerService.commit(ownerUri); + + // Dispose + const disposeResult: EditDisposeResult = {}; + mockClient.sendRequest + .withArgs(EditDisposeRequest.type, sinon.match.any) + .resolves(disposeResult); + await tableExplorerService.dispose(ownerUri); + + expect(mockClient.sendRequest.callCount).to.equal(5); + }); + + test("should handle row operations in sequence", async () => { + const ownerUri = "row-ops-uri"; + + // Create row + const createResult: EditCreateRowResult = { + defaultValues: [], + newRowId: 1, + row: { + cells: [], + id: 1, + isDirty: true, + state: EditRowState.dirtyInsert, + }, + }; + mockClient.sendRequest + .withArgs(EditCreateRowRequest.type, sinon.match.any) + .resolves(createResult); + const newRow = await tableExplorerService.createRow(ownerUri); + + // Update cell in new row + const updateResult: EditUpdateCellResult = { + cell: { + displayValue: "Value", + isNull: false, + invariantCultureDisplayValue: "Value", + isDirty: true, + }, + isRowDirty: true, + }; + mockClient.sendRequest + .withArgs(EditUpdateCellRequest.type, sinon.match.any) + .resolves(updateResult); + await tableExplorerService.updateCell(ownerUri, newRow.newRowId, 0, "Value"); + + // Revert row + const revertResult: EditRevertRowResult = { + row: { cells: [], id: 1, isDirty: false, state: EditRowState.clean }, + }; + mockClient.sendRequest + .withArgs(EditRevertRowRequest.type, sinon.match.any) + .resolves(revertResult); + await tableExplorerService.revertRow(ownerUri, newRow.newRowId); + + expect(mockClient.sendRequest.callCount).to.equal(3); + }); + }); +}); diff --git a/test/unit/tableExplorerWebViewController.test.ts b/test/unit/tableExplorerWebViewController.test.ts new file mode 100644 index 0000000000..8e3c3630e2 --- /dev/null +++ b/test/unit/tableExplorerWebViewController.test.ts @@ -0,0 +1,827 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from "assert"; +import * as sinon from "sinon"; +import * as vscode from "vscode"; +import { TableExplorerWebViewController } from "../../src/tableExplorer/tableExplorerWebViewController"; +import { ITableExplorerService } from "../../src/services/tableExplorerService"; +import ConnectionManager from "../../src/controllers/connectionManager"; +import VscodeWrapper from "../../src/controllers/vscodeWrapper"; +import { TreeNodeInfo } from "../../src/objectExplorer/nodes/treeNodeInfo"; +import { + EditSubsetResult, + EditRow, + EditRowState, + EditCreateRowResult, + EditCellResult, + EditRevertRowResult, + EditScriptResult, + EditSessionReadyParams, + DbCellValue, +} from "../../src/sharedInterfaces/tableExplorer"; +import { IConnectionProfile } from "../../src/models/interfaces"; +import * as LocConstants from "../../src/constants/locConstants"; +import { stubTelemetry, stubVscodeWrapper } from "./utils"; + +suite("TableExplorerWebViewController - Reducers", () => { + let sandbox: sinon.SinonSandbox; + let mockContext: vscode.ExtensionContext; + let mockVscodeWrapper: VscodeWrapper; + let mockTableExplorerService: sinon.SinonStubbedInstance; + let mockConnectionManager: sinon.SinonStubbedInstance; + let mockTargetNode: TreeNodeInfo; + let controller: TableExplorerWebViewController; + let mockWebview: vscode.Webview; + let mockPanel: vscode.WebviewPanel; + let showInformationMessageStub: sinon.SinonStub; + let showWarningMessageStub: sinon.SinonStub; + let showErrorMessageStub: sinon.SinonStub; + let openTextDocumentStub: sinon.SinonStub; + let showTextDocumentStub: sinon.SinonStub; + let writeTextStub: sinon.SinonStub; + + const mockConnectionProfile: IConnectionProfile = { + server: "test-server", + database: "test-db", + authenticationType: "SqlLogin", + user: "test-user", + password: "test-password", + connectionString: "", + profileName: "test-profile", + savePassword: true, + emptyPasswordInput: false, + } as IConnectionProfile; + + const createMockCell = (displayValue: string, isNull: boolean = false): DbCellValue => ({ + displayValue, + isNull, + invariantCultureDisplayValue: displayValue, + }); + + const createMockRow = (id: number, cellValues: string[]): EditRow => ({ + id, + isDirty: false, + state: EditRowState.clean, + cells: cellValues.map((val) => createMockCell(val)), + }); + + const createMockSubsetResult = (rowCount: number = 2): EditSubsetResult => ({ + rowCount, + subset: [createMockRow(0, ["1", "John", "Doe"]), createMockRow(1, ["2", "Jane", "Smith"])], + columnInfo: [ + { name: "id", isEditable: false }, + { name: "firstName", isEditable: true }, + { name: "lastName", isEditable: true }, + ], + }); + + setup(() => { + sandbox = sinon.createSandbox(); + + // Mock vscode.window methods + showInformationMessageStub = sandbox.stub(vscode.window, "showInformationMessage"); + showWarningMessageStub = sandbox.stub(vscode.window, "showWarningMessage"); + showErrorMessageStub = sandbox.stub(vscode.window, "showErrorMessage"); + openTextDocumentStub = sandbox.stub(vscode.workspace, "openTextDocument"); + showTextDocumentStub = sandbox.stub(vscode.window, "showTextDocument"); + writeTextStub = sandbox.stub(); + sandbox.stub(vscode.env, "clipboard").value({ + writeText: writeTextStub, + }); + + // Setup mock webview and panel + mockWebview = { + postMessage: sandbox.stub(), + asWebviewUri: sandbox.stub().returns(vscode.Uri.parse("file:///webview")), + onDidReceiveMessage: sandbox.stub(), + } as any; + + mockPanel = { + webview: mockWebview, + title: "Test Panel", + viewColumn: vscode.ViewColumn.One, + options: {}, + reveal: sandbox.stub(), + dispose: sandbox.stub(), + onDidDispose: sandbox.stub(), + onDidChangeViewState: sandbox.stub(), + iconPath: undefined, + } as any; + + sandbox.stub(vscode.window, "createWebviewPanel").returns(mockPanel); + stubTelemetry(sandbox); + + // Setup mock context + mockContext = { + extensionUri: vscode.Uri.parse("file:///test"), + extensionPath: "/test", + subscriptions: [], + } as unknown as vscode.ExtensionContext; + + // Setup mock services + mockVscodeWrapper = stubVscodeWrapper(sandbox); + mockTableExplorerService = { + initialize: sandbox.stub().resolves(), + subset: sandbox.stub().resolves(createMockSubsetResult()), + commit: sandbox.stub().resolves({}), + createRow: sandbox.stub().resolves(), + deleteRow: sandbox.stub().resolves({}), + updateCell: sandbox.stub().resolves(), + revertCell: sandbox.stub().resolves(), + revertRow: sandbox.stub().resolves(), + generateScripts: sandbox.stub().resolves(), + dispose: sandbox.stub().resolves({}), + sqlToolsClient: { + onNotification: sandbox.stub(), + } as any, + } as any; + + mockConnectionManager = { + isConnected: sandbox.stub().returns(true), + isConnecting: sandbox.stub().returns(false), + connect: sandbox.stub().resolves(), + } as any; + + mockTargetNode = { + metadata: { + name: "TestTable", + schema: "dbo", + metadataTypeName: "Table", + }, + connectionProfile: mockConnectionProfile, + } as any; + + // Create controller + controller = new TableExplorerWebViewController( + mockContext, + mockVscodeWrapper, + mockTableExplorerService, + mockConnectionManager, + mockTargetNode, + ); + + // Simulate edit session ready + const notificationHandler = (mockTableExplorerService.sqlToolsClient.onNotification as any) + .firstCall.args[1]; + notificationHandler({ + ownerUri: "test-owner-uri", + success: true, + message: "", + } as EditSessionReadyParams); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite("commitChanges reducer", () => { + test("should commit changes successfully and clear newRows", async () => { + // Arrange + controller.state.ownerUri = "test-owner-uri"; + controller.state.newRows = [createMockRow(100, ["3", "New", "Row"])]; + mockTableExplorerService.commit.resolves({}); + + // Act + await controller["_reducerHandlers"].get("commitChanges")(controller.state, {}); + + // Assert + assert.ok(mockTableExplorerService.commit.calledOnceWith("test-owner-uri")); + assert.ok( + showInformationMessageStub.calledOnceWith( + LocConstants.TableExplorer.changesSavedSuccessfully, + ), + ); + assert.strictEqual(controller.state.newRows.length, 0); + }); + + test("should show error message when commit fails", async () => { + // Arrange + controller.state.ownerUri = "test-owner-uri"; + const error = new Error("Commit failed"); + mockTableExplorerService.commit.rejects(error); + + // Act + await controller["_reducerHandlers"].get("commitChanges")(controller.state, {}); + + // Assert + assert.ok(mockTableExplorerService.commit.calledOnce); + assert.ok(showErrorMessageStub.calledOnce); + assert.ok(showErrorMessageStub.firstCall.args[0].includes("Failed to save changes")); + }); + }); + + suite("loadSubset reducer", () => { + test("should load subset with specified row count", async () => { + // Arrange + controller.state.ownerUri = "test-owner-uri"; + controller.state.newRows = []; + const mockResult = createMockSubsetResult(5); + mockTableExplorerService.subset.resolves(mockResult); + mockTableExplorerService.subset.resetHistory(); // Reset call history from initialization + + // Act + await controller["_reducerHandlers"].get("loadSubset")(controller.state, { + rowCount: 100, + }); + + // Assert + sinon.assert.calledOnce(mockTableExplorerService.subset); + sinon.assert.calledWith(mockTableExplorerService.subset, "test-owner-uri", 0, 100); + assert.strictEqual(controller.state.currentRowCount, 100); + assert.strictEqual(controller.state.resultSet?.rowCount, 2); + }); + + test("should append newRows to subset result", async () => { + // Arrange + controller.state.ownerUri = "test-owner-uri"; + const newRow = createMockRow(100, ["3", "New", "Row"]); + controller.state.newRows = [newRow]; + const mockResult = createMockSubsetResult(2); + mockTableExplorerService.subset.resolves(mockResult); + + // Act + await controller["_reducerHandlers"].get("loadSubset")(controller.state, { + rowCount: 50, + }); + + // Assert + assert.strictEqual(controller.state.resultSet?.rowCount, 3); // 2 from DB + 1 new + assert.strictEqual(controller.state.resultSet?.subset.length, 3); + assert.strictEqual(controller.state.resultSet?.subset[2].id, 100); + }); + + test("should show error message when loadSubset fails", async () => { + // Arrange + controller.state.ownerUri = "test-owner-uri"; + const error = new Error("Load failed"); + mockTableExplorerService.subset.rejects(error); + + // Act + await controller["_reducerHandlers"].get("loadSubset")(controller.state, { + rowCount: 100, + }); + + // Assert + assert.ok(showErrorMessageStub.calledOnce); + assert.ok(showErrorMessageStub.firstCall.args[0].includes("Failed to load data")); + }); + }); + + suite("createRow reducer", () => { + test("should create a new row and add it to resultSet", async () => { + // Arrange + controller.state.ownerUri = "test-owner-uri"; + controller.state.resultSet = createMockSubsetResult(2); + const newRow = createMockRow(100, ["", "", ""]); + const createRowResult: EditCreateRowResult = { + newRowId: 100, + row: newRow, + defaultValues: ["", "", ""], + }; + mockTableExplorerService.createRow.resolves(createRowResult); + + // Act + await controller["_reducerHandlers"].get("createRow")(controller.state, {}); + + // Assert + assert.ok(mockTableExplorerService.createRow.calledOnceWith("test-owner-uri")); + assert.ok( + showInformationMessageStub.calledOnceWith( + LocConstants.TableExplorer.rowCreatedSuccessfully, + ), + ); + assert.strictEqual(controller.state.newRows.length, 1); + assert.strictEqual(controller.state.newRows[0].id, 100); + assert.strictEqual(controller.state.resultSet?.rowCount, 3); + }); + + test("should regenerate script if script pane is visible", async () => { + // Arrange + controller.state.ownerUri = "test-owner-uri"; + controller.state.resultSet = createMockSubsetResult(2); + controller.state.showScriptPane = true; + const newRow = createMockRow(100, ["", "", ""]); + const createRowResult: EditCreateRowResult = { + newRowId: 100, + row: newRow, + defaultValues: ["", "", ""], + }; + mockTableExplorerService.createRow.resolves(createRowResult); + mockTableExplorerService.generateScripts.resolves({ + scripts: ["INSERT INTO TestTable VALUES (...)"], + }); + + // Act + await controller["_reducerHandlers"].get("createRow")(controller.state, {}); + + // Assert + assert.ok(mockTableExplorerService.generateScripts.calledOnce); + }); + + test("should show error message when createRow fails", async () => { + // Arrange + controller.state.ownerUri = "test-owner-uri"; + controller.state.resultSet = createMockSubsetResult(2); + const error = new Error("Create row failed"); + mockTableExplorerService.createRow.rejects(error); + showErrorMessageStub.resetHistory(); // Reset call history from previous tests + + // Act + await controller["_reducerHandlers"].get("createRow")(controller.state, {}); + + // Assert + sinon.assert.calledOnce(showErrorMessageStub); + assert.ok( + showErrorMessageStub.firstCall.args[0].includes("Failed to create a new row"), + ); + }); + }); + + suite("deleteRow reducer", () => { + test("should delete a row from resultSet", async () => { + // Arrange + controller.state.ownerUri = "test-owner-uri"; + controller.state.resultSet = createMockSubsetResult(2); + mockTableExplorerService.deleteRow.resolves({}); + + // Act + await controller["_reducerHandlers"].get("deleteRow")(controller.state, { rowId: 0 }); + + // Assert + assert.ok(mockTableExplorerService.deleteRow.calledOnceWith("test-owner-uri", 0)); + assert.ok( + showInformationMessageStub.calledOnceWith(LocConstants.TableExplorer.rowRemoved), + ); + assert.strictEqual(controller.state.resultSet?.rowCount, 1); + assert.strictEqual(controller.state.resultSet?.subset.length, 1); + }); + + test("should remove row from newRows array if it's a new row", async () => { + // Arrange + controller.state.ownerUri = "test-owner-uri"; + const newRow = createMockRow(100, ["3", "New", "Row"]); + controller.state.newRows = [newRow]; + controller.state.resultSet = { + ...createMockSubsetResult(2), + subset: [...createMockSubsetResult(2).subset, newRow], + rowCount: 3, + }; + mockTableExplorerService.deleteRow.resolves({}); + + // Act + await controller["_reducerHandlers"].get("deleteRow")(controller.state, { rowId: 100 }); + + // Assert + assert.strictEqual(controller.state.newRows.length, 0); + assert.strictEqual(controller.state.resultSet?.rowCount, 2); + }); + + test("should regenerate script if script pane is visible", async () => { + // Arrange + controller.state.ownerUri = "test-owner-uri"; + controller.state.resultSet = createMockSubsetResult(2); + controller.state.showScriptPane = true; + mockTableExplorerService.deleteRow.resolves({}); + mockTableExplorerService.generateScripts.resolves({ + scripts: ["DELETE FROM TestTable WHERE id = 1"], + }); + + // Act + await controller["_reducerHandlers"].get("deleteRow")(controller.state, { rowId: 0 }); + + // Assert + assert.ok(mockTableExplorerService.generateScripts.calledOnce); + }); + + test("should show error message when deleteRow fails", async () => { + // Arrange + controller.state.ownerUri = "test-owner-uri"; + const error = new Error("Delete row failed"); + mockTableExplorerService.deleteRow.rejects(error); + + // Act + await controller["_reducerHandlers"].get("deleteRow")(controller.state, { rowId: 0 }); + + // Assert + assert.ok(showErrorMessageStub.calledOnce); + assert.ok(showErrorMessageStub.firstCall.args[0].includes("Failed to remove row")); + }); + }); + + suite("updateCell reducer", () => { + test("should update cell value in resultSet", async () => { + // Arrange + controller.state.ownerUri = "test-owner-uri"; + controller.state.resultSet = createMockSubsetResult(2); + const updatedCell: EditCellResult = { + cell: { + displayValue: "Updated", + isNull: false, + invariantCultureDisplayValue: "Updated", + isDirty: true, + }, + isRowDirty: true, + }; + mockTableExplorerService.updateCell.resolves(updatedCell); + + // Act + await controller["_reducerHandlers"].get("updateCell")(controller.state, { + rowId: 0, + columnId: 1, + newValue: "Updated", + }); + + // Assert + assert.ok( + mockTableExplorerService.updateCell.calledOnceWith( + "test-owner-uri", + 0, + 1, + "Updated", + ), + ); + assert.strictEqual( + controller.state.resultSet?.subset[0].cells[1].displayValue, + "Updated", + ); + }); + + test("should regenerate script if script pane is visible", async () => { + // Arrange + controller.state.ownerUri = "test-owner-uri"; + controller.state.resultSet = createMockSubsetResult(2); + controller.state.showScriptPane = true; + const updatedCell: EditCellResult = { + cell: { + displayValue: "Updated", + isNull: false, + invariantCultureDisplayValue: "Updated", + isDirty: true, + }, + isRowDirty: true, + }; + mockTableExplorerService.updateCell.resolves(updatedCell); + mockTableExplorerService.generateScripts.resolves({ + scripts: ["UPDATE TestTable SET firstName = 'Updated' WHERE id = 1"], + }); + + // Act + await controller["_reducerHandlers"].get("updateCell")(controller.state, { + rowId: 0, + columnId: 1, + newValue: "Updated", + }); + + // Assert + assert.ok(mockTableExplorerService.generateScripts.calledOnce); + }); + + test("should show error message when updateCell fails", async () => { + // Arrange + controller.state.ownerUri = "test-owner-uri"; + const error = new Error("Update cell failed"); + mockTableExplorerService.updateCell.rejects(error); + + // Act + await controller["_reducerHandlers"].get("updateCell")(controller.state, { + rowId: 0, + columnId: 1, + newValue: "Updated", + }); + + // Assert + assert.ok(showErrorMessageStub.calledOnce); + assert.ok(showErrorMessageStub.firstCall.args[0].includes("Failed to update cell")); + }); + }); + + suite("revertCell reducer", () => { + test("should revert cell to original value", async () => { + // Arrange + controller.state.ownerUri = "test-owner-uri"; + controller.state.resultSet = createMockSubsetResult(2); + const revertedCell: EditCellResult = { + cell: { + displayValue: "John", + isNull: false, + invariantCultureDisplayValue: "John", + isDirty: false, + }, + isRowDirty: false, + }; + mockTableExplorerService.revertCell.resolves(revertedCell); + + // Act + await controller["_reducerHandlers"].get("revertCell")(controller.state, { + rowId: 0, + columnId: 1, + }); + + // Assert + assert.ok(mockTableExplorerService.revertCell.calledOnceWith("test-owner-uri", 0, 1)); + assert.strictEqual(controller.state.resultSet?.subset[0].cells[1].displayValue, "John"); + }); + + test("should regenerate script if script pane is visible", async () => { + // Arrange + controller.state.ownerUri = "test-owner-uri"; + controller.state.resultSet = createMockSubsetResult(2); + controller.state.showScriptPane = true; + const revertedCell: EditCellResult = { + cell: { + displayValue: "John", + isNull: false, + invariantCultureDisplayValue: "John", + isDirty: false, + }, + isRowDirty: false, + }; + mockTableExplorerService.revertCell.resolves(revertedCell); + mockTableExplorerService.generateScripts.resolves({ scripts: [] }); + + // Act + await controller["_reducerHandlers"].get("revertCell")(controller.state, { + rowId: 0, + columnId: 1, + }); + + // Assert + assert.ok(mockTableExplorerService.generateScripts.calledOnce); + }); + + test("should show error message when revertCell fails", async () => { + // Arrange + controller.state.ownerUri = "test-owner-uri"; + const error = new Error("Revert cell failed"); + mockTableExplorerService.revertCell.rejects(error); + + // Act + await controller["_reducerHandlers"].get("revertCell")(controller.state, { + rowId: 0, + columnId: 1, + }); + + // Assert + assert.ok(showErrorMessageStub.calledOnce); + assert.ok(showErrorMessageStub.firstCall.args[0].includes("Failed to revert cell")); + }); + }); + + suite("revertRow reducer", () => { + test("should revert entire row to original values", async () => { + // Arrange + controller.state.ownerUri = "test-owner-uri"; + controller.state.resultSet = createMockSubsetResult(2); + const revertedRow: EditRevertRowResult = { + row: createMockRow(0, ["1", "John", "Doe"]), + }; + mockTableExplorerService.revertRow.resolves(revertedRow); + + // Act + await controller["_reducerHandlers"].get("revertRow")(controller.state, { rowId: 0 }); + + // Assert + assert.ok(mockTableExplorerService.revertRow.calledOnceWith("test-owner-uri", 0)); + assert.strictEqual(controller.state.resultSet?.subset[0].cells[1].displayValue, "John"); + }); + + test("should regenerate script if script pane is visible", async () => { + // Arrange + controller.state.ownerUri = "test-owner-uri"; + controller.state.resultSet = createMockSubsetResult(2); + controller.state.showScriptPane = true; + const revertedRow: EditRevertRowResult = { + row: createMockRow(0, ["1", "John", "Doe"]), + }; + mockTableExplorerService.revertRow.resolves(revertedRow); + mockTableExplorerService.generateScripts.resolves({ scripts: [] }); + + // Act + await controller["_reducerHandlers"].get("revertRow")(controller.state, { rowId: 0 }); + + // Assert + assert.ok(mockTableExplorerService.generateScripts.calledOnce); + }); + + test("should show error message when revertRow fails", async () => { + // Arrange + controller.state.ownerUri = "test-owner-uri"; + const error = new Error("Revert row failed"); + mockTableExplorerService.revertRow.rejects(error); + + // Act + await controller["_reducerHandlers"].get("revertRow")(controller.state, { rowId: 0 }); + + // Assert + assert.ok(showErrorMessageStub.calledOnce); + assert.ok(showErrorMessageStub.firstCall.args[0].includes("Failed to revert row")); + }); + }); + + suite("generateScript reducer", () => { + test("should generate script and show script pane", async () => { + // Arrange + controller.state.ownerUri = "test-owner-uri"; + controller.state.showScriptPane = false; + const scriptResult: EditScriptResult = { + scripts: [ + "UPDATE TestTable SET firstName = 'Updated' WHERE id = 1;", + "DELETE FROM TestTable WHERE id = 2;", + ], + }; + mockTableExplorerService.generateScripts.resolves(scriptResult); + + // Act + await controller["_reducerHandlers"].get("generateScript")(controller.state, {}); + + // Assert + assert.ok(mockTableExplorerService.generateScripts.calledOnceWith("test-owner-uri")); + assert.ok(controller.state.updateScript?.includes("UPDATE TestTable")); + assert.ok(controller.state.updateScript?.includes("DELETE FROM TestTable")); + assert.strictEqual(controller.state.showScriptPane, true); + }); + + test("should handle empty script array", async () => { + // Arrange + controller.state.ownerUri = "test-owner-uri"; + const scriptResult: EditScriptResult = { + scripts: [], + }; + mockTableExplorerService.generateScripts.resolves(scriptResult); + + // Act + await controller["_reducerHandlers"].get("generateScript")(controller.state, {}); + + // Assert + assert.strictEqual(controller.state.updateScript, ""); + assert.strictEqual(controller.state.showScriptPane, true); + }); + + test("should show error message when generateScript fails", async () => { + // Arrange + controller.state.ownerUri = "test-owner-uri"; + const error = new Error("Generate script failed"); + mockTableExplorerService.generateScripts.rejects(error); + + // Act + await controller["_reducerHandlers"].get("generateScript")(controller.state, {}); + + // Assert + assert.ok(showErrorMessageStub.calledOnce); + assert.ok(showErrorMessageStub.firstCall.args[0].includes("Failed to generate script")); + }); + }); + + suite("openScriptInEditor reducer", () => { + test("should open script in SQL editor", async () => { + // Arrange + controller.state.updateScript = "SELECT * FROM TestTable;"; + const mockDocument = {} as vscode.TextDocument; + openTextDocumentStub.resolves(mockDocument); + showTextDocumentStub.resolves(); + + // Act + await controller["_reducerHandlers"].get("openScriptInEditor")(controller.state, {}); + + // Assert + assert.ok(openTextDocumentStub.calledOnce); + const callArgs = openTextDocumentStub.firstCall.args[0]; + assert.strictEqual(callArgs.content, "SELECT * FROM TestTable;"); + assert.strictEqual(callArgs.language, "sql"); + assert.ok(showTextDocumentStub.calledOnceWith(mockDocument)); + }); + + test("should show warning when no script to open", async () => { + // Arrange + controller.state.updateScript = undefined; + + // Act + await controller["_reducerHandlers"].get("openScriptInEditor")(controller.state, {}); + + // Assert + assert.ok(openTextDocumentStub.notCalled); + assert.ok( + showWarningMessageStub.calledOnceWith(LocConstants.TableExplorer.noScriptToOpen), + ); + }); + + test("should show error message when opening script fails", async () => { + // Arrange + controller.state.updateScript = "SELECT * FROM TestTable;"; + const error = new Error("Open failed"); + openTextDocumentStub.rejects(error); + + // Act + await controller["_reducerHandlers"].get("openScriptInEditor")(controller.state, {}); + + // Assert + assert.ok(showErrorMessageStub.calledOnce); + assert.ok(showErrorMessageStub.firstCall.args[0].includes("Failed to open script")); + }); + }); + + suite("copyScriptToClipboard reducer", () => { + test("should copy script to clipboard", async () => { + // Arrange + controller.state.updateScript = "SELECT * FROM TestTable;"; + writeTextStub.resolves(); + + // Act + await controller["_reducerHandlers"].get("copyScriptToClipboard")(controller.state, {}); + + // Assert + assert.ok(writeTextStub.calledOnceWith("SELECT * FROM TestTable;")); + assert.ok( + showInformationMessageStub.calledOnceWith( + LocConstants.TableExplorer.scriptCopiedToClipboard, + ), + ); + }); + + test("should show warning when no script to copy", async () => { + // Arrange + controller.state.updateScript = undefined; + + // Act + await controller["_reducerHandlers"].get("copyScriptToClipboard")(controller.state, {}); + + // Assert + assert.ok(writeTextStub.notCalled); + assert.ok( + showWarningMessageStub.calledOnceWith(LocConstants.TableExplorer.noScriptToCopy), + ); + }); + + test("should show error message when copying script fails", async () => { + // Arrange + controller.state.updateScript = "SELECT * FROM TestTable;"; + const error = new Error("Copy failed"); + writeTextStub.rejects(error); + + // Act + await controller["_reducerHandlers"].get("copyScriptToClipboard")(controller.state, {}); + + // Assert + assert.ok(showErrorMessageStub.calledOnce); + assert.ok(showErrorMessageStub.firstCall.args[0].includes("Failed to copy script")); + }); + }); + + suite("toggleScriptPane reducer", () => { + test("should toggle script pane from false to true", async () => { + // Arrange + controller.state.showScriptPane = false; + + // Act + await controller["_reducerHandlers"].get("toggleScriptPane")(controller.state, {}); + + // Assert + assert.strictEqual(controller.state.showScriptPane, true); + }); + + test("should toggle script pane from true to false", async () => { + // Arrange + controller.state.showScriptPane = true; + + // Act + await controller["_reducerHandlers"].get("toggleScriptPane")(controller.state, {}); + + // Assert + assert.strictEqual(controller.state.showScriptPane, false); + }); + }); + + suite("setCurrentPage reducer", () => { + test("should set current page number", async () => { + // Arrange + controller.state.currentPage = 1; + + // Act + await controller["_reducerHandlers"].get("setCurrentPage")(controller.state, { + pageNumber: 5, + }); + + // Assert + assert.strictEqual(controller.state.currentPage, 5); + }); + + test("should update page number multiple times", async () => { + // Arrange + controller.state.currentPage = 1; + + // Act & Assert + await controller["_reducerHandlers"].get("setCurrentPage")(controller.state, { + pageNumber: 2, + }); + assert.strictEqual(controller.state.currentPage, 2); + + await controller["_reducerHandlers"].get("setCurrentPage")(controller.state, { + pageNumber: 10, + }); + assert.strictEqual(controller.state.currentPage, 10); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 5ba0913aea..3ff5588956 100644 --- a/yarn.lock +++ b/yarn.lock @@ -718,6 +718,13 @@ "@eslint/core" "^0.15.1" levn "^0.4.1" +"@excel-builder-vanilla/types@^4.1.0": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@excel-builder-vanilla/types/-/types-4.2.1.tgz#663e913bb88087bde5bf42f0f8be9d1bd2762bfd" + integrity sha512-AtVzHKfH7TtRTH7Yczwu6SMXYhmvO+W6H2L4ktg7JesK4cHSS7FinzFk+zkPWs2ROZEXYHLZxJTGY7OrhiOTjw== + dependencies: + fflate "^0.8.2" + "@floating-ui/core@^1.7.0": version "1.7.0" resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.7.0.tgz#1aff27a993ea1b254a586318c29c3b16ea0f4d0a" @@ -1778,6 +1785,11 @@ dependencies: "@swc/helpers" "^0.5.1" +"@formkit/tempo@^0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@formkit/tempo/-/tempo-0.1.2.tgz#85ff615dddc6cc55ab9f9ca83cf9a41cfdf778fc" + integrity sha512-jNPPbjL8oj7hK3eHX++CwbR6X4GKQt+x00/q4yeXkwynXHGKL27dylYhpEgwrmediPP4y7s0XtN1if/M/JYujg== + "@griffel/core@^1.16.0", "@griffel/core@^1.17.0": version "1.17.0" resolved "https://registry.yarnpkg.com/@griffel/core/-/core-1.17.0.tgz#34ecd744d5ad3ab6a59f5e0b2d9ef485ae39b510" @@ -2022,6 +2034,74 @@ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== +"@slickgrid-universal/binding@9.8.0": + version "9.8.0" + resolved "https://registry.yarnpkg.com/@slickgrid-universal/binding/-/binding-9.8.0.tgz#e0cea592e104d90f47daaa2a9ab0648603720059" + integrity sha512-FN3zOGeyzPHbMql7TVLj2LPtr0kFsNBMshTIsKb2SW9ldn0XAp9yi9rhdg9s7TozhIyUvMN5xDj8YnKeo4V3Pg== + +"@slickgrid-universal/common@9.8.0": + version "9.8.0" + resolved "https://registry.yarnpkg.com/@slickgrid-universal/common/-/common-9.8.0.tgz#5ae491da2a44249e3738ec3c0eecd2d731b49285" + integrity sha512-wAxeHzgJ4tjAt5dDLnFa5HVREwjaZYEd8RhNOaLkKWht58x5tiatP3lDkgEk0iWtkIsai1Mhww3wbrileG2eUQ== + dependencies: + "@excel-builder-vanilla/types" "^4.1.0" + "@formkit/tempo" "^0.1.2" + "@slickgrid-universal/binding" "9.8.0" + "@slickgrid-universal/event-pub-sub" "9.8.0" + "@slickgrid-universal/utils" "9.8.0" + "@types/sortablejs" "^1.15.8" + "@types/trusted-types" "^2.0.7" + autocompleter "^9.3.2" + dequal "^2.0.3" + multiple-select-vanilla "^4.3.8" + sortablejs "^1.15.6" + un-flatten-tree "^2.0.12" + vanilla-calendar-pro "^3.0.5" + +"@slickgrid-universal/custom-footer-component@9.8.0": + version "9.8.0" + resolved "https://registry.yarnpkg.com/@slickgrid-universal/custom-footer-component/-/custom-footer-component-9.8.0.tgz#ff86a524962a860fcdc639d9a6a1f6ce9d8ac4ab" + integrity sha512-5yqLWXLS3Qu7fHSfS/02ReH1desz2Y3l84a4G2ee8/Op0Dbm6ZL03QCn60sWJXt+2zh5uTpLT3pXNECFSWQWVA== + dependencies: + "@formkit/tempo" "^0.1.2" + "@slickgrid-universal/binding" "9.8.0" + "@slickgrid-universal/common" "9.8.0" + +"@slickgrid-universal/empty-warning-component@9.8.0": + version "9.8.0" + resolved "https://registry.yarnpkg.com/@slickgrid-universal/empty-warning-component/-/empty-warning-component-9.8.0.tgz#c5fa5a6fc00259037bad9c0ad6efed69033cede2" + integrity sha512-X33l+OWgseV0/QEfNFavTRC/RvVbCB1nQkBDKB/nnu83npc36q+UkYd0U4hqPI9Hm2VNsZKfVEY0tb97Z+XGpQ== + dependencies: + "@slickgrid-universal/common" "9.8.0" + +"@slickgrid-universal/event-pub-sub@9.8.0": + version "9.8.0" + resolved "https://registry.yarnpkg.com/@slickgrid-universal/event-pub-sub/-/event-pub-sub-9.8.0.tgz#59db8cd103cf1bc9f61db3f9afc48e222e971cee" + integrity sha512-z/hSgV/D2jYLVvHRp1HxDnsAksMg2i9gdKhblcD8P7CPwCphttaXt22w9DJmaIHqwe629mMUwmWfFpp0jcYMQg== + dependencies: + "@slickgrid-universal/utils" "9.8.0" + +"@slickgrid-universal/pagination-component@9.8.0": + version "9.8.0" + resolved "https://registry.yarnpkg.com/@slickgrid-universal/pagination-component/-/pagination-component-9.8.0.tgz#e98bb95b4c6a7fa4ca4e2c90866f80ea7d84cd11" + integrity sha512-upR7qfujxqMgW66C3L0/feoiZcDJsXzHiSyVcRFuqvZyTt9TBrbK6OfjtkQgukN5EF1xDXjKgeZiiPlaZ5CfzQ== + dependencies: + "@slickgrid-universal/binding" "9.8.0" + "@slickgrid-universal/common" "9.8.0" + +"@slickgrid-universal/row-detail-view-plugin@9.8.0": + version "9.8.0" + resolved "https://registry.yarnpkg.com/@slickgrid-universal/row-detail-view-plugin/-/row-detail-view-plugin-9.8.0.tgz#ce1032302c10b14eed4cae00ca8a3f44e2269a1f" + integrity sha512-2zqa1kXQHeDey7B+wkyujDvjeG2NIgBOzOqfyLdZQm97VGAMY7XrXT91RNxK98ZHUM5hckKdoNOFZQmThxob0w== + dependencies: + "@slickgrid-universal/common" "9.8.0" + "@slickgrid-universal/utils" "9.8.0" + +"@slickgrid-universal/utils@9.8.0": + version "9.8.0" + resolved "https://registry.yarnpkg.com/@slickgrid-universal/utils/-/utils-9.8.0.tgz#7edd9e8905dc9aa9f7b312d8faddc31347724b46" + integrity sha512-Ht/3ljDiqdbJrRxWPVTcrfIe4TMOWXtzcFgltakErZi7+UMHaS0J9s5BoI8ITYLeA7XvAOamT35OT0oRFDgmDA== + "@stylistic/eslint-plugin@^2.8.0": version "2.8.0" resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin/-/eslint-plugin-2.8.0.tgz#9fcbcf8b4b27cc3867eedce37b8c8fded1010107" @@ -2288,11 +2368,21 @@ resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.3.tgz#ff5e2f1902969d305225a047c8a0fd5c915cebef" integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ== +"@types/sortablejs@^1.15.8": + version "1.15.8" + resolved "https://registry.yarnpkg.com/@types/sortablejs/-/sortablejs-1.15.8.tgz#11ed555076046e00869a5ef85d1e7651e7a66ef6" + integrity sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg== + "@types/tmp@0.0.28": version "0.0.28" resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.0.28.tgz#94d829cb6f035afed5129fd92c4f2783d5c12bf5" integrity sha1-lNgpy28DWv7VEp/ZLE8ng9XBK/U= +"@types/trusted-types@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" + integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== + "@types/tunnel@0.0.1": version "0.0.1" resolved "https://registry.yarnpkg.com/@types/tunnel/-/tunnel-0.0.1.tgz#0d72774768b73df26f25df9184273a42da72b19c" @@ -2938,6 +3028,11 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= +autocompleter@^9.3.2: + version "9.3.2" + resolved "https://registry.yarnpkg.com/autocompleter/-/autocompleter-9.3.2.tgz#b66d4a2d19a6b6c378c6a897bf3c000eccef5fb9" + integrity sha512-rLbf2TLGOD7y+gOS36ksrZdIsvoHa2KXc2A7503w+NBRPrcF73zzFeYBxEcV/iMPjaBH3jFhNIYObZ7zt1fkCQ== + available-typed-arrays@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" @@ -3716,7 +3811,7 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= -dequal@^2.0.0: +dequal@^2.0.0, dequal@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== @@ -4452,6 +4547,11 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" +fflate@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea" + integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A== + figures@^1.4.0: version "1.7.0" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" @@ -6456,6 +6556,13 @@ ms@^2.1.1, ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +multiple-select-vanilla@^4.3.8: + version "4.3.8" + resolved "https://registry.yarnpkg.com/multiple-select-vanilla/-/multiple-select-vanilla-4.3.8.tgz#145819029e9670d880e8e4e8b04b9f31465c6085" + integrity sha512-n/QUXSszEUEhtKTu8pnkVpY3xP9WVH3SdvsaGsu4vVqz+IvontFend+zR6BgGzXwCdafrwzCVW8jAbMNGN/u6w== + dependencies: + "@types/trusted-types" "^2.0.7" + mute-stream@~0.0.4: version "0.0.8" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" @@ -7683,10 +7790,30 @@ slice-ansi@^7.1.0: ansi-styles "^6.2.1" is-fullwidth-code-point "^5.0.0" +slickgrid-react@^9.8.0: + version "9.8.0" + resolved "https://registry.yarnpkg.com/slickgrid-react/-/slickgrid-react-9.8.0.tgz#006f0248f643b4b44bd3ad4699074825db48c535" + integrity sha512-aKGp8xzy8x566Oc4B//o4tnl5wwt8xx1K25LEccLdZ1exmnCQMkTHu9Icg6PMKljnrTQmjFSv3s+mZ+B/oeqhQ== + dependencies: + "@slickgrid-universal/common" "9.8.0" + "@slickgrid-universal/custom-footer-component" "9.8.0" + "@slickgrid-universal/empty-warning-component" "9.8.0" + "@slickgrid-universal/event-pub-sub" "9.8.0" + "@slickgrid-universal/pagination-component" "9.8.0" + "@slickgrid-universal/row-detail-view-plugin" "9.8.0" + "@slickgrid-universal/utils" "9.8.0" + dequal "^2.0.3" + sortablejs "^1.15.6" + "slickgrid@github:Microsoft/SlickGrid.ADS#2.3.49": version "2.3.49" resolved "https://codeload.github.com/Microsoft/SlickGrid.ADS/tar.gz/a3c833d6d0c9e629a064975f146bb648206bc7b8" +sortablejs@^1.15.6: + version "1.15.6" + resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.15.6.tgz#ff93699493f5b8ab8d828f933227b4988df1d393" + integrity sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A== + source-map-support@^0.5.21: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" @@ -8296,6 +8423,11 @@ uc.micro@^2.0.0, uc.micro@^2.1.0: resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee" integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== +un-flatten-tree@^2.0.12: + version "2.0.12" + resolved "https://registry.yarnpkg.com/un-flatten-tree/-/un-flatten-tree-2.0.12.tgz#8d92ec454a1b7e1aead948ed029907e1cb9a9ed8" + integrity sha512-E7v59ADEqVQs9gTZYxoe3uGs6Jj/a3gJ7lSJaTIBTc5w0+B3PJ/kVjs/Y/A26NBWEW8WAo556PpRatH4XHZR1w== + unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" @@ -8449,6 +8581,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +vanilla-calendar-pro@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/vanilla-calendar-pro/-/vanilla-calendar-pro-3.0.5.tgz#4832d508a7dcb7eeb7e104c8397d08526f99b64e" + integrity sha512-4X9bmTo1/KzbZrB7B6mZXtvVXIhcKxaVSnFZuaVtps7tshKJDxgaIElkgdia6IjB5qWetWuu7kZ+ZaV1sPxy6w== + vfile-message@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.2.tgz#c883c9f677c72c166362fd635f21fc165a7d1181"