diff --git a/packages/server/src/integrations/microsoftSqlServer.ts b/packages/server/src/integrations/microsoftSqlServer.ts index 291aad8631..6038b842c4 100644 --- a/packages/server/src/integrations/microsoftSqlServer.ts +++ b/packages/server/src/integrations/microsoftSqlServer.ts @@ -43,6 +43,7 @@ const SCHEMA: Integration = { features: { [DatasourceFeature.CONNECTION_CHECKING]: true, [DatasourceFeature.FETCH_TABLE_NAMES]: true, + [DatasourceFeature.EXPORT_SCHEMA]: true, }, datasource: { user: { @@ -336,6 +337,81 @@ class SqlServerIntegration extends Sql implements DatasourcePlus { result.recordset ? result.recordset : [{ [operation]: true }] return this.queryWithReturning(json, queryFn, processFn) } + + async getExternalSchema() { + // Query to retrieve table schema + const query = ` + SELECT + t.name AS TableName, + c.name AS ColumnName, + ty.name AS DataType, + c.max_length AS MaxLength, + c.is_nullable AS IsNullable, + c.is_identity AS IsIdentity + FROM + sys.tables t + INNER JOIN sys.columns c ON t.object_id = c.object_id + INNER JOIN sys.types ty ON c.system_type_id = ty.system_type_id + WHERE + t.is_ms_shipped = 0 + ORDER BY + t.name, c.column_id +` + + await this.connect() + + const result = await this.internalQuery({ + sql: query, + }) + + const scriptParts = [] + const tables: any = {} + for (const row of result.recordset) { + const { + TableName, + ColumnName, + DataType, + MaxLength, + IsNullable, + IsIdentity, + } = row + + if (!tables[TableName]) { + tables[TableName] = { + columns: [], + } + } + + const columnDefinition = `${ColumnName} ${DataType}${ + MaxLength ? `(${MaxLength})` : "" + }${IsNullable ? " NULL" : " NOT NULL"}` + + tables[TableName].columns.push(columnDefinition) + + if (IsIdentity) { + tables[TableName].identityColumn = ColumnName + } + } + + // Generate SQL statements for table creation + for (const tableName in tables) { + const { columns, identityColumn } = tables[tableName] + + let createTableStatement = `CREATE TABLE [${tableName}] (\n` + createTableStatement += columns.join(",\n") + + if (identityColumn) { + createTableStatement += `,\n CONSTRAINT [PK_${tableName}] PRIMARY KEY (${identityColumn})` + } + + createTableStatement += "\n);" + + scriptParts.push(createTableStatement) + } + + const schema = scriptParts.join("\n") + return schema + } } export default { diff --git a/qa-core/src/integrations/external-schema/mssql.integration.spec.ts b/qa-core/src/integrations/external-schema/mssql.integration.spec.ts new file mode 100644 index 0000000000..450b093cf7 --- /dev/null +++ b/qa-core/src/integrations/external-schema/mssql.integration.spec.ts @@ -0,0 +1,112 @@ +import { GenericContainer, Wait } from "testcontainers" +import { Duration, TemporalUnit } from "node-duration" +import mssql from "../../../../packages/server/src/integrations/microsoftSqlServer" + +jest.unmock("mssql") + +describe("getExternalSchema", () => { + describe("postgres", () => { + let config: any + + beforeAll(async () => { + const password = "Str0Ng_p@ssW0rd!" + const container = await new GenericContainer( + "mcr.microsoft.com/mssql/server" + ) + .withExposedPorts(1433) + .withEnv("ACCEPT_EULA", "Y") + .withEnv("MSSQL_SA_PASSWORD", password) + .withEnv("MSSQL_PID", "Developer") + .withWaitStrategy(Wait.forHealthCheck()) + .withHealthCheck({ + test: `/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "${password}" -Q "SELECT 1" -b -o /dev/null`, + interval: new Duration(1000, TemporalUnit.MILLISECONDS), + timeout: new Duration(3, TemporalUnit.SECONDS), + retries: 20, + startPeriod: new Duration(100, TemporalUnit.MILLISECONDS), + }) + .start() + + const host = container.getContainerIpAddress() + const port = container.getMappedPort(1433) + config = { + user: "sa", + password, + server: host, + port: port, + database: "master", + schema: "dbo", + } + }) + + it("can export an empty database", async () => { + const integration = new mssql.integration(config) + const result = await integration.getExternalSchema() + expect(result).toMatchInlineSnapshot(`""`) + }) + + it("can export a database with tables", async () => { + const integration = new mssql.integration(config) + + await integration.connect() + await integration.internalQuery({ + sql: ` + CREATE TABLE users ( + id INT IDENTITY(1,1) PRIMARY KEY, + name VARCHAR(100) NOT NULL, + role VARCHAR(15) NOT NULL + ); + + CREATE TABLE products ( + id INT IDENTITY(1,1) PRIMARY KEY, + name VARCHAR(100) NOT NULL, + price DECIMAL(10, 2) NOT NULL + ); + `, + }) + + const result = await integration.getExternalSchema() + expect(result).toMatchInlineSnapshot(` + "CREATE TABLE [products] ( + id int(4) NOT NULL, + name varchar(100) NOT NULL, + price decimal(9) NOT NULL, + CONSTRAINT [PK_products] PRIMARY KEY (id) + ); + CREATE TABLE [users] ( + id int(4) NOT NULL, + name varchar(100) NOT NULL, + role varchar(15) NOT NULL, + CONSTRAINT [PK_users] PRIMARY KEY (id) + );" + `) + }) + + it("does not export a data", async () => { + const integration = new mssql.integration(config) + + await integration.connect() + await integration.internalQuery({ + sql: `INSERT INTO [users] ([name], [role]) VALUES ('John Doe', 'Administrator'); + INSERT INTO [products] ([name], [price]) VALUES ('Book', 7.68); + `, + }) + + const result = await integration.getExternalSchema() + expect(result).toMatchInlineSnapshot(` + "CREATE TABLE [products] ( + id int(4) NOT NULL, + name varchar(100) NOT NULL, + price decimal(9) NOT NULL, + CONSTRAINT [PK_products] PRIMARY KEY (id) + ); + CREATE TABLE [users] ( + id int(4) NOT NULL, + name varchar(100) NOT NULL, + role varchar(15) NOT NULL, + CONSTRAINT [PK_users] PRIMARY KEY (id) + );" + `) + }) + }) +})