feat(mssql): add MSSQL writer for generating DDL from database schema
All checks were successful
All checks were successful
- Implement MSSQL writer to generate SQL scripts for creating schemas, tables, and constraints. - Support for identity columns, indexes, and extended properties. - Add tests for column definitions, table creation, primary keys, foreign keys, and comments. - Include testing guide and sample schema for integration tests.
This commit is contained in:
286
test_data/mssql/TESTING.md
Normal file
286
test_data/mssql/TESTING.md
Normal file
@@ -0,0 +1,286 @@
|
||||
# MSSQL Reader and Writer Testing Guide
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker and Docker Compose installed
|
||||
- RelSpec binary built (`make build`)
|
||||
- jq (optional, for JSON processing)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Start SQL Server Express
|
||||
|
||||
```bash
|
||||
docker-compose up -d mssql
|
||||
|
||||
# Wait for container to be healthy
|
||||
docker-compose ps
|
||||
|
||||
# Monitor startup logs
|
||||
docker-compose logs -f mssql
|
||||
```
|
||||
|
||||
### 2. Verify Database Creation
|
||||
|
||||
```bash
|
||||
docker exec -it $(docker-compose ps -q mssql) \
|
||||
/opt/mssql-tools/bin/sqlcmd \
|
||||
-S localhost \
|
||||
-U sa \
|
||||
-P 'StrongPassword123!' \
|
||||
-Q "SELECT name FROM sys.databases WHERE name = 'RelSpecTest'"
|
||||
```
|
||||
|
||||
## Testing Scenarios
|
||||
|
||||
### Scenario 1: Read MSSQL Database to JSON
|
||||
|
||||
Read the test schema from MSSQL and export to JSON:
|
||||
|
||||
```bash
|
||||
./build/relspec convert \
|
||||
--from mssql \
|
||||
--from-conn "sqlserver://sa:StrongPassword123!@localhost:1433/RelSpecTest" \
|
||||
--to json \
|
||||
--to-path test_output.json
|
||||
```
|
||||
|
||||
Verify output:
|
||||
```bash
|
||||
jq '.Schemas[0].Tables | length' test_output.json
|
||||
jq '.Schemas[0].Tables[0]' test_output.json
|
||||
```
|
||||
|
||||
### Scenario 2: Read MSSQL Database to DBML
|
||||
|
||||
Convert MSSQL schema to DBML format:
|
||||
|
||||
```bash
|
||||
./build/relspec convert \
|
||||
--from mssql \
|
||||
--from-conn "sqlserver://sa:StrongPassword123!@localhost:1433/RelSpecTest" \
|
||||
--to dbml \
|
||||
--to-path test_output.dbml
|
||||
```
|
||||
|
||||
### Scenario 3: Generate SQL Script (No Direct Execution)
|
||||
|
||||
Generate SQL script without executing:
|
||||
|
||||
```bash
|
||||
./build/relspec convert \
|
||||
--from mssql \
|
||||
--from-conn "sqlserver://sa:StrongPassword123!@localhost:1433/RelSpecTest" \
|
||||
--to mssql \
|
||||
--to-path test_output.sql
|
||||
```
|
||||
|
||||
Inspect generated SQL:
|
||||
```bash
|
||||
head -50 test_output.sql
|
||||
```
|
||||
|
||||
### Scenario 4: Round-Trip Conversion (MSSQL → JSON → MSSQL)
|
||||
|
||||
Test bidirectional conversion:
|
||||
|
||||
```bash
|
||||
# Step 1: MSSQL → JSON
|
||||
./build/relspec convert \
|
||||
--from mssql \
|
||||
--from-conn "sqlserver://sa:StrongPassword123!@localhost:1433/RelSpecTest" \
|
||||
--to json \
|
||||
--to-path backup.json
|
||||
|
||||
# Step 2: JSON → MSSQL SQL
|
||||
./build/relspec convert \
|
||||
--from json \
|
||||
--from-path backup.json \
|
||||
--to mssql \
|
||||
--to-path restore.sql
|
||||
|
||||
# Inspect SQL
|
||||
cat restore.sql | head -50
|
||||
```
|
||||
|
||||
### Scenario 5: Cross-Database Conversion
|
||||
|
||||
If you have PostgreSQL running, test conversion:
|
||||
|
||||
```bash
|
||||
# MSSQL → PostgreSQL SQL
|
||||
./build/relspec convert \
|
||||
--from mssql \
|
||||
--from-conn "sqlserver://sa:StrongPassword123!@localhost:1433/RelSpecTest" \
|
||||
--to pgsql \
|
||||
--to-path mssql_to_pg.sql
|
||||
```
|
||||
|
||||
### Scenario 6: Test Type Mappings
|
||||
|
||||
Create a JSON file with various types and convert to MSSQL:
|
||||
|
||||
```json
|
||||
{
|
||||
"Name": "TypeTest",
|
||||
"Schemas": [
|
||||
{
|
||||
"Name": "dbo",
|
||||
"Tables": [
|
||||
{
|
||||
"Name": "type_samples",
|
||||
"Columns": {
|
||||
"id": {
|
||||
"Name": "id",
|
||||
"Type": "int",
|
||||
"AutoIncrement": true,
|
||||
"NotNull": true,
|
||||
"Sequence": 1
|
||||
},
|
||||
"big_num": {
|
||||
"Name": "big_num",
|
||||
"Type": "int64",
|
||||
"Sequence": 2
|
||||
},
|
||||
"is_active": {
|
||||
"Name": "is_active",
|
||||
"Type": "bool",
|
||||
"Sequence": 3
|
||||
},
|
||||
"description": {
|
||||
"Name": "description",
|
||||
"Type": "text",
|
||||
"Sequence": 4
|
||||
},
|
||||
"created_at": {
|
||||
"Name": "created_at",
|
||||
"Type": "timestamp",
|
||||
"NotNull": true,
|
||||
"Default": "GETDATE()",
|
||||
"Sequence": 5
|
||||
},
|
||||
"unique_id": {
|
||||
"Name": "unique_id",
|
||||
"Type": "uuid",
|
||||
"Sequence": 6
|
||||
},
|
||||
"metadata": {
|
||||
"Name": "metadata",
|
||||
"Type": "json",
|
||||
"Sequence": 7
|
||||
},
|
||||
"binary_data": {
|
||||
"Name": "binary_data",
|
||||
"Type": "bytea",
|
||||
"Sequence": 8
|
||||
}
|
||||
},
|
||||
"Constraints": {
|
||||
"PK_type_samples_id": {
|
||||
"Name": "PK_type_samples_id",
|
||||
"Type": "PRIMARY_KEY",
|
||||
"Columns": ["id"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Convert to MSSQL:
|
||||
```bash
|
||||
./build/relspec convert \
|
||||
--from json \
|
||||
--from-path type_test.json \
|
||||
--to mssql \
|
||||
--to-path type_test.sql
|
||||
|
||||
cat type_test.sql
|
||||
```
|
||||
|
||||
## Cleanup
|
||||
|
||||
Stop and remove the SQL Server container:
|
||||
|
||||
```bash
|
||||
docker-compose down
|
||||
|
||||
# Clean up test files
|
||||
rm -f test_output.* backup.json restore.sql
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Container won't start
|
||||
|
||||
Check Docker daemon is running and database logs:
|
||||
```bash
|
||||
docker-compose logs mssql
|
||||
```
|
||||
|
||||
### Connection refused errors
|
||||
|
||||
Wait for container to be healthy:
|
||||
```bash
|
||||
docker-compose ps
|
||||
# Wait until STATUS shows "healthy"
|
||||
|
||||
# Or check manually
|
||||
docker exec -it $(docker-compose ps -q mssql) \
|
||||
/opt/mssql-tools/bin/sqlcmd \
|
||||
-S localhost \
|
||||
-U sa \
|
||||
-P 'StrongPassword123!' \
|
||||
-Q "SELECT @@VERSION"
|
||||
```
|
||||
|
||||
### Test schema not found
|
||||
|
||||
Initialize the test schema:
|
||||
```bash
|
||||
docker exec -i $(docker-compose ps -q mssql) \
|
||||
/opt/mssql-tools/bin/sqlcmd \
|
||||
-S localhost \
|
||||
-U sa \
|
||||
-P 'StrongPassword123!' \
|
||||
< test_data/mssql/test_schema.sql
|
||||
```
|
||||
|
||||
### Connection string format issues
|
||||
|
||||
Use the correct format for connection strings:
|
||||
- Default port: 1433
|
||||
- Username: `sa`
|
||||
- Password: `StrongPassword123!`
|
||||
- Database: `RelSpecTest`
|
||||
|
||||
Format: `sqlserver://sa:StrongPassword123!@localhost:1433/RelSpecTest`
|
||||
|
||||
## Performance Notes
|
||||
|
||||
- Initial reader setup may take a few seconds
|
||||
- Type mapping queries are cached within a single read operation
|
||||
- Direct execution mode is atomic per table/constraint
|
||||
- Large schemas (100+ tables) should complete in under 5 seconds
|
||||
|
||||
## Unit Test Verification
|
||||
|
||||
Run the MSSQL-specific tests:
|
||||
|
||||
```bash
|
||||
# Type mapping tests
|
||||
go test ./pkg/mssql/... -v
|
||||
|
||||
# Reader tests
|
||||
go test ./pkg/readers/mssql/... -v
|
||||
|
||||
# Writer tests
|
||||
go test ./pkg/writers/mssql/... -v
|
||||
|
||||
# All together
|
||||
go test ./pkg/mssql/... ./pkg/readers/mssql/... ./pkg/writers/mssql/... -v
|
||||
```
|
||||
|
||||
Expected output: All tests should PASS
|
||||
187
test_data/mssql/test_schema.sql
Normal file
187
test_data/mssql/test_schema.sql
Normal file
@@ -0,0 +1,187 @@
|
||||
-- Test schema for MSSQL Reader integration tests
|
||||
-- This script creates a sample database for testing the MSSQL reader
|
||||
|
||||
USE master;
|
||||
GO
|
||||
|
||||
-- Drop existing database if it exists
|
||||
IF EXISTS (SELECT 1 FROM sys.databases WHERE name = 'RelSpecTest')
|
||||
BEGIN
|
||||
ALTER DATABASE RelSpecTest SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
|
||||
DROP DATABASE RelSpecTest;
|
||||
END
|
||||
GO
|
||||
|
||||
-- Create test database
|
||||
CREATE DATABASE RelSpecTest;
|
||||
GO
|
||||
|
||||
USE RelSpecTest;
|
||||
GO
|
||||
|
||||
-- Create schemas
|
||||
CREATE SCHEMA [public];
|
||||
GO
|
||||
|
||||
CREATE SCHEMA [auth];
|
||||
GO
|
||||
|
||||
-- Create tables in public schema
|
||||
CREATE TABLE [public].[users] (
|
||||
[id] INT IDENTITY(1,1) NOT NULL,
|
||||
[email] NVARCHAR(255) NOT NULL,
|
||||
[username] NVARCHAR(100) NOT NULL,
|
||||
[created_at] DATETIME2 NOT NULL DEFAULT GETDATE(),
|
||||
[updated_at] DATETIME2 NULL,
|
||||
[is_active] BIT NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY ([id]),
|
||||
UNIQUE ([email]),
|
||||
UNIQUE ([username])
|
||||
);
|
||||
GO
|
||||
|
||||
CREATE TABLE [public].[posts] (
|
||||
[id] INT IDENTITY(1,1) NOT NULL,
|
||||
[user_id] INT NOT NULL,
|
||||
[title] NVARCHAR(255) NOT NULL,
|
||||
[content] NVARCHAR(MAX) NOT NULL,
|
||||
[published_at] DATETIME2 NULL,
|
||||
[created_at] DATETIME2 NOT NULL DEFAULT GETDATE(),
|
||||
PRIMARY KEY ([id])
|
||||
);
|
||||
GO
|
||||
|
||||
CREATE TABLE [public].[comments] (
|
||||
[id] INT IDENTITY(1,1) NOT NULL,
|
||||
[post_id] INT NOT NULL,
|
||||
[user_id] INT NOT NULL,
|
||||
[content] NVARCHAR(MAX) NOT NULL,
|
||||
[created_at] DATETIME2 NOT NULL DEFAULT GETDATE(),
|
||||
PRIMARY KEY ([id])
|
||||
);
|
||||
GO
|
||||
|
||||
-- Create tables in auth schema
|
||||
CREATE TABLE [auth].[roles] (
|
||||
[id] INT IDENTITY(1,1) NOT NULL,
|
||||
[name] NVARCHAR(100) NOT NULL,
|
||||
[description] NVARCHAR(MAX) NULL,
|
||||
PRIMARY KEY ([id]),
|
||||
UNIQUE ([name])
|
||||
);
|
||||
GO
|
||||
|
||||
CREATE TABLE [auth].[user_roles] (
|
||||
[id] INT IDENTITY(1,1) NOT NULL,
|
||||
[user_id] INT NOT NULL,
|
||||
[role_id] INT NOT NULL,
|
||||
PRIMARY KEY ([id]),
|
||||
UNIQUE ([user_id], [role_id])
|
||||
);
|
||||
GO
|
||||
|
||||
-- Add foreign keys
|
||||
ALTER TABLE [public].[posts]
|
||||
ADD CONSTRAINT [FK_posts_users]
|
||||
FOREIGN KEY ([user_id])
|
||||
REFERENCES [public].[users] ([id])
|
||||
ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||
GO
|
||||
|
||||
ALTER TABLE [public].[comments]
|
||||
ADD CONSTRAINT [FK_comments_posts]
|
||||
FOREIGN KEY ([post_id])
|
||||
REFERENCES [public].[posts] ([id])
|
||||
ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||
GO
|
||||
|
||||
ALTER TABLE [public].[comments]
|
||||
ADD CONSTRAINT [FK_comments_users]
|
||||
FOREIGN KEY ([user_id])
|
||||
REFERENCES [public].[users] ([id])
|
||||
ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||
GO
|
||||
|
||||
ALTER TABLE [auth].[user_roles]
|
||||
ADD CONSTRAINT [FK_user_roles_users]
|
||||
FOREIGN KEY ([user_id])
|
||||
REFERENCES [public].[users] ([id])
|
||||
ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||
GO
|
||||
|
||||
ALTER TABLE [auth].[user_roles]
|
||||
ADD CONSTRAINT [FK_user_roles_roles]
|
||||
FOREIGN KEY ([role_id])
|
||||
REFERENCES [auth].[roles] ([id])
|
||||
ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||
GO
|
||||
|
||||
-- Create indexes
|
||||
CREATE INDEX [IDX_users_email] ON [public].[users] ([email]);
|
||||
GO
|
||||
|
||||
CREATE INDEX [IDX_posts_user_id] ON [public].[posts] ([user_id]);
|
||||
GO
|
||||
|
||||
CREATE INDEX [IDX_comments_post_id] ON [public].[comments] ([post_id]);
|
||||
GO
|
||||
|
||||
CREATE INDEX [IDX_comments_user_id] ON [public].[comments] ([user_id]);
|
||||
GO
|
||||
|
||||
-- Add extended properties (comments)
|
||||
EXEC sp_addextendedproperty
|
||||
@name = 'MS_Description',
|
||||
@value = 'User accounts table',
|
||||
@level0type = 'SCHEMA', @level0name = 'public',
|
||||
@level1type = 'TABLE', @level1name = 'users';
|
||||
GO
|
||||
|
||||
EXEC sp_addextendedproperty
|
||||
@name = 'MS_Description',
|
||||
@value = 'User unique identifier',
|
||||
@level0type = 'SCHEMA', @level0name = 'public',
|
||||
@level1type = 'TABLE', @level1name = 'users',
|
||||
@level2type = 'COLUMN', @level2name = 'id';
|
||||
GO
|
||||
|
||||
EXEC sp_addextendedproperty
|
||||
@name = 'MS_Description',
|
||||
@value = 'User email address',
|
||||
@level0type = 'SCHEMA', @level0name = 'public',
|
||||
@level1type = 'TABLE', @level1name = 'users',
|
||||
@level2type = 'COLUMN', @level2name = 'email';
|
||||
GO
|
||||
|
||||
EXEC sp_addextendedproperty
|
||||
@name = 'MS_Description',
|
||||
@value = 'Blog posts table',
|
||||
@level0type = 'SCHEMA', @level0name = 'public',
|
||||
@level1type = 'TABLE', @level1name = 'posts';
|
||||
GO
|
||||
|
||||
EXEC sp_addextendedproperty
|
||||
@name = 'MS_Description',
|
||||
@value = 'User roles mapping table',
|
||||
@level0type = 'SCHEMA', @level0name = 'auth',
|
||||
@level1type = 'TABLE', @level1name = 'user_roles';
|
||||
GO
|
||||
|
||||
-- Add check constraint
|
||||
ALTER TABLE [public].[users]
|
||||
ADD CONSTRAINT [CK_users_email_format]
|
||||
CHECK (LEN(email) > 0 AND email LIKE '%@%.%');
|
||||
GO
|
||||
|
||||
-- Verify schema was created
|
||||
SELECT
|
||||
SCHEMA_NAME(s.schema_id) as [Schema],
|
||||
t.name as [Table],
|
||||
COUNT(c.column_id) as [ColumnCount]
|
||||
FROM sys.tables t
|
||||
INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
|
||||
LEFT JOIN sys.columns c ON t.object_id = c.object_id
|
||||
WHERE SCHEMA_NAME(s.schema_id) IN ('public', 'auth')
|
||||
GROUP BY SCHEMA_NAME(s.schema_id), t.name
|
||||
ORDER BY [Schema], [Table];
|
||||
GO
|
||||
Reference in New Issue
Block a user