Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# DLSync Changelog

This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.0.0] - 2026-01-15
### Added
- Added support for account level objects like database, schemas, roles, warehouses etc.
## [2.6.1] - 2026-01-06
### Fixed
- Fixed private key authentication issue with parameter names
Expand Down
213 changes: 149 additions & 64 deletions README.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CREATE OR REPLACE API INTEGRATION AWS_API_GATEWAY
API_PROVIDER = aws_api_gateway
API_AWS_ROLE_ARN = 'arn:aws:iam::123456789012:role/my_cloud_account_role'
API_ALLOWED_PREFIXES = ('https://xyz.execute-api.us-west-2.amazonaws.com/production')
ENABLED = TRUE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CREATE OR REPLACE AUTHENTICATION POLICY mfa_policy
AUTHENTICATION_METHODS = ('PASSWORD', 'KEYPAIR')
MFA_ENROLLMENT = 'REQUIRED'
CLIENT_TYPES = ('ALL')
COMMENT = 'Authentication policy requiring MFA';
4 changes: 4 additions & 0 deletions example_scripts/main/ACCOUNT/DATABASES/${EXAMPLE_DB}.SQL
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---version: 0, author: DlSync
CREATE DATABASE IF NOT EXISTS ${EXAMPLE_DB};
---rollback: DROP DATABASE IF EXISTS ${EXAMPLE_DB} CASCADE;
---verify: SHOW DATABASES LIKE '${EXAMPLE_DB}';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
CREATE OR REPLACE NETWORK POLICY allow_internal_ips
ALLOWED_NETWORK_RULE_LIST = (${EXAMPLE_DB}.${MAIN_SCHEMA}.INTERNAL_IPS_RULE)
COMMENT = 'Network policy for internal access only';
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CREATE OR REPLACE NOTIFICATION INTEGRATION email_notification
TYPE = 'EMAIL'
ENABLED = TRUE
COMMENT = 'Email notification integration for alerts';


Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CREATE OR REPLACE PASSWORD POLICY password_strength_policy
PASSWORD_MIN_LENGTH = 12
PASSWORD_MAX_LENGTH = 256
PASSWORD_MIN_UPPER_CASE_CHARS = 1
PASSWORD_MIN_LOWER_CASE_CHARS = 1
PASSWORD_MIN_NUMERIC_CHARS = 1
PASSWORD_MIN_SPECIAL_CHARS = 1
PASSWORD_HISTORY = 5
COMMENT = 'Password policy enforcing strong passwords';
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CREATE OR REPLACE RESOURCE MONITOR compute_monitor
CREDIT_QUOTA = 100
TRIGGERS ON 50 PERCENT DO NOTIFY
ON 75 PERCENT DO NOTIFY
ON 100 PERCENT DO SUSPEND;
14 changes: 14 additions & 0 deletions example_scripts/main/ACCOUNT/ROLES/USER_ROLE.SQL
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---version: 0, author: DlSync
CREATE OR REPLACE ROLE USER_ROLE;
---verify: SHOW ROLES LIKE 'USER_ROLE';
---rollback: DROP ROLE IF EXISTS USER_ROLE;

---version: 1, author: DlSync
GRANT USAGE ON DATABASE ${EXAMPLE_DB} TO ROLE USER_ROLE;
---verify: SHOW GRANTS TO ROLE USER_ROLE;
---rollback: REVOKE USAGE ON DATABASE ${EXAMPLE_DB} FROM ROLE USER_ROLE;

---version: 2, author: DlSync
GRANT USAGE ON SCHEMA ${EXAMPLE_DB}.${MAIN_SCHEMA} TO ROLE USER_ROLE;
---verify: SHOW GRANTS TO ROLE USER_ROLE;
---rollback: REVOKE USAGE ON SCHEMA ${EXAMPLE_DB}.${MAIN_SCHEMA} FROM ROLE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---version: 0, author: DlSync
CREATE SCHEMA IF NOT EXISTS ${EXAMPLE_DB}.${AUDIT_SCHEMA};
---rollback: DROP SCHEMA IF EXISTS ${EXAMPLE_DB}.${AUDIT_SCHEMA} CASCADE;
---verify: SHOW SCHEMA LIKE '${EXAMPLE_DB}.${AUDIT_SCHEMA}';
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---version: 0, author: DlSync
CREATE SCHEMA IF NOT EXISTS ${EXAMPLE_DB}.${MAIN_SCHEMA};
---rollback: DROP SCHEMA IF EXISTS ${EXAMPLE_DB}.${MAIN_SCHEMA} CASCADE;
---verify: SHOW SCHEMA LIKE '${EXAMPLE_DB}.${MAIN_SCHEMA}';
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE OR REPLACE SECURITY INTEGRATION oauth_integration
TYPE = oauth
ENABLED = true
OAUTH_CLIENT = custom
OAUTH_CLIENT_TYPE = 'CONFIDENTIAL'
OAUTH_REDIRECT_URI = 'https://localhost.com'
OAUTH_ISSUE_REFRESH_TOKENS = TRUE
OAUTH_REFRESH_TOKEN_VALIDITY = 86400;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
CREATE OR REPLACE SESSION POLICY session_timeout_policy
SESSION_IDLE_TIMEOUT_MINS = 60
SESSION_UI_IDLE_TIMEOUT_MINS = 15
COMMENT = 'Session timeout policy for security';
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
CREATE OR REPLACE STORAGE INTEGRATION s3_integration
TYPE = 'EXTERNAL_STAGE'
ENABLED = FALSE
STORAGE_PROVIDER = 'S3'
STORAGE_AWS_ROLE_ARN = 'arn:aws:iam::123456789012:role/snowflake-role'
STORAGE_ALLOWED_LOCATIONS = ('s3://my-bucket/data/')
COMMENT = 'S3 storage integration for data ingestion';
8 changes: 8 additions & 0 deletions example_scripts/main/ACCOUNT/WAREHOUSES/EXAMPLE_WH.SQL
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE OR REPLACE WAREHOUSE example_wh
WAREHOUSE_SIZE = 'XSMALL'
WAREHOUSE_TYPE = 'STANDARD'
AUTO_SUSPEND = 300
AUTO_RESUME = TRUE
INITIALLY_SUSPENDED = TRUE
RESOURCE_MONITOR = compute_monitor
COMMENT = 'Example warehouse for demonstration purposes';
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
CREATE OR REPLACE NETWORK RULE ${EXAMPLE_DB}.${MAIN_SCHEMA}.INTERNAL_IPS_RULE
MODE = INGRESS
TYPE = IPV4
VALUE_LIST = ('192.168.0.0/24', '192.168.1.0/24')
COMMENT = 'Example network rule for ingress traffic'
;

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
create or replace streamlit ${EXAMPLE_DB}.${AUDIT_SCHEMA}.PRODUCT_DASH_BOARD
create or replace streamlit ${EXAMPLE_DB}.${MAIN_SCHEMA}.PRODUCT_DASH_BOARD
root_location=@${EXAMPLE_DB}.${MAIN_SCHEMA}.PRODUCT_DATA_STAGE
main_file='/streamlit_app.py'
query_warehouse='${MY_WAREHOUSE}'
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
releaseVersion=2.6.1
releaseVersion=3.0.0
18 changes: 9 additions & 9 deletions src/main/java/com/snowflake/dlsync/ChangeManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@
import com.snowflake.dlsync.doa.ScriptSource;
import com.snowflake.dlsync.models.*;
import com.snowflake.dlsync.parser.ParameterInjector;
import com.snowflake.dlsync.parser.TestQueryGenerator;
import lombok.extern.slf4j.Slf4j;

import java.io.*;
import java.security.NoSuchAlgorithmException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Slf4j
public class ChangeManager {
Expand Down Expand Up @@ -92,7 +91,7 @@ public void rollback() throws SQLException, IOException {
startSync(ChangeType.ROLLBACK);
Set<String> deployedScriptIds = new HashSet<>(scriptRepo.loadScriptHash());
scriptSource.getAllScripts().forEach(script -> deployedScriptIds.remove(script.getId()));
List<MigrationScript> migrations = scriptRepo.getMigrationScripts(deployedScriptIds);
List<MigrationScript> migrations = scriptRepo.getDeployedMigrationScripts(deployedScriptIds);
dependencyGraph.addNodes(migrations);

List<Script> changedScripts = scriptSource.getAllScripts()
Expand Down Expand Up @@ -135,12 +134,13 @@ public boolean verify() throws IOException, NoSuchAlgorithmException, SQLExcepti

List<String> schemaNames = scriptRepo.getAllSchemasInDatabase(scriptRepo.getDatabaseName());
for(String schema: schemaNames) {
List<Script> stateScripts = scriptRepo.getStateScriptsInSchema(schema)
List<SchemaScript> declarativeScripts = scriptRepo.getAllScriptsInSchema(schema)
.stream()
.filter(script -> !script.isMigration())
.filter(script -> !config.isScriptExcluded(script))
.collect(Collectors.toList());

for(Script script: stateScripts) {
for(SchemaScript script: declarativeScripts) {
parameterInjector.parametrizeScript(script, true);
Script sourceScript = sourceScripts.stream().filter(s -> s.equals(script)).findFirst().orElse(null);
if(sourceScript == null) {
Expand All @@ -160,7 +160,7 @@ public boolean verify() throws IOException, NoSuchAlgorithmException, SQLExcepti
Map<String, List<MigrationScript>> groupedMigrationScripts = sourceScripts.stream()
.filter(script -> script instanceof MigrationScript)
.map(script -> (MigrationScript)script)
.collect(Collectors.groupingBy(Script::getObjectName));
.collect(Collectors.groupingBy(MigrationScript::getFullObjectName));

for(String objectName: groupedMigrationScripts.keySet()) {
List<MigrationScript> sameObjectMigrations = groupedMigrationScripts.get(objectName);
Expand Down Expand Up @@ -204,15 +204,15 @@ public void createAllScriptsFromDB(String targetSchemas) throws SQLException, IO
}
int count = 0;
for(String schema: schemaNames) {
List<Script> scripts = scriptRepo.getAllScriptsInSchema(schema);
for(Script script: scripts) {
List<SchemaScript> scripts = scriptRepo.getAllScriptsInSchema(schema);
for(SchemaScript script: scripts) {
count++;
if(configTables.contains(script.getFullObjectName())) {
scriptRepo.addConfig(script);
}
parameterInjector.parametrizeScript(script, false);
}
scriptSource.createScriptFiles(scripts);
scriptSource.createSchemaScriptFiles(scripts);
}
endSyncSuccess(ChangeType.CREATE_SCRIPT, (long)count);

Expand Down
71 changes: 45 additions & 26 deletions src/main/java/com/snowflake/dlsync/ScriptFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,34 @@
import com.snowflake.dlsync.models.*;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

@Slf4j
public class ScriptFactory {
public static StateScript getStateScript(String databaseName, String schemaName, ScriptObjectType objectType, String objectName, String content) {
return new StateScript(databaseName, schemaName, objectName, objectType, content);

public static AccountScript getAccountScript(String scriptPath, ScriptObjectType objectType, String objectName, String content) {
return new AccountScript(scriptPath, objectName, objectType, content);
}

public static SchemaScript getSchemaScript(String scriptPath, String databaseName, String schemaName, ScriptObjectType objectType, String objectName, String content) {
return new SchemaScript(scriptPath, databaseName, schemaName, objectName, objectType, content);
}

public static StateScript getStateScript(String scriptPath, String databaseName, String schemaName, ScriptObjectType objectType, String objectName, String content) {
return new StateScript(scriptPath, databaseName, schemaName, objectName, objectType, content);
public static SchemaScript getSchemaScript(String databaseName, String schemaName, ScriptObjectType objectType, String objectName, String content) {
return getSchemaScript(null, databaseName, schemaName, objectType, objectName, content);
}

public static MigrationScript getMigrationScript(String scriptPath, String databaseName, String schemaName, ScriptObjectType objectType, String objectName, String content, Long version, String author, String rollback, String verify) {
return new MigrationScript(scriptPath, databaseName, schemaName, objectName, objectType, content, version, author, rollback, verify);
public static MigrationScript getMigrationScript(Script parentScript, String content, Long version, String author, String rollback, String verify) {
return new MigrationScript(parentScript, content, version, author, rollback, verify);
}

public static MigrationScript getMigrationScript(String databaseName, String schemaName, ScriptObjectType objectType, String objectName, String content, Long version, String author, String rollback, String verify) {
return new MigrationScript(databaseName, schemaName, objectName, objectType, content, version, author, rollback, verify);
public static MigrationScript getSchemaMigrationScript(String databaseName, String schemaName, ScriptObjectType objectType, String objectName, String content, Long version, String author, String rollback, String verify) {
SchemaScript schemaScript = getSchemaScript(databaseName, schemaName, objectType, objectName, content);
return new MigrationScript(schemaScript, content, version, author, rollback, verify);
}

public static MigrationScript getMigrationScript(String fullObjectName, ScriptObjectType objectType, String content, Long version, String author, String rollback, String verify) {
public static MigrationScript getSchemaMigrationScript(String fullObjectName, ScriptObjectType objectType, String content, Long version, String author, String rollback, String verify) {
String databaseName = null, schemaName = null, objectName = null;
String[] nameSplit = fullObjectName.split("\\.");
if(nameSplit.length > 2) {
Expand All @@ -33,28 +42,38 @@ public static MigrationScript getMigrationScript(String fullObjectName, ScriptOb
log.error("Error while splitting fullObjectName {}: Missing some values", fullObjectName);
throw new RuntimeException("Error while splitting fullObjectName");
}
return new MigrationScript(databaseName, schemaName, objectName, objectType, content, version, author, rollback, verify);
}
public static MigrationScript getMigrationScript(String database, String schema, ScriptObjectType type, String objectName, String content) {
Long version = 0L;
String author = "DlSync";
String rollback = String.format("DROP %s IF EXISTS %s;", type.getSingular(), database + "." + schema + "." + objectName);
String verify = String.format("SHOW %s LIKE '%s';",type, database + "." + schema + "." + objectName);
return getSchemaMigrationScript(databaseName, schemaName, objectType, objectName, content, version, author, rollback, verify);

String migrationHeader = String.format("---version: %s, author: %s\n", version, author);
String rollbackFormat = String.format("\n---rollback: %s", rollback);
String verifyFormat = String.format("\n---verify: %s", verify);
content = migrationHeader + content + rollbackFormat + verifyFormat;
MigrationScript script = getMigrationScript(database, schema, type, objectName, content, version, author, rollback, verify);
return script;
}

public static MigrationScript getMigrationScript(String databaseName, String schemaName, ScriptObjectType objectType, String objectName, Migration migration) {
return new MigrationScript(databaseName, schemaName, objectName, objectType, migration.getContent(), migration.getVersion(), migration.getAuthor(), migration.getRollback(), migration.getVerify());


public static MigrationScript getMigrationScript(Script parentScript, Migration migration) {
return new MigrationScript(parentScript, migration.getContent(), migration.getVersion(), migration.getAuthor(), migration.getRollback(), migration.getVerify());
}

public static TestScript getTestScript(String scriptPath, String databaseName, String schemaName, ScriptObjectType objectType, String objectName, String content, Script script) {
return new TestScript(scriptPath, databaseName, schemaName, objectName, objectType, content, script);
public static List<MigrationScript> getMigrationScripts(Script parentScript, List<Migration> migrations) {
if(!parentScript.getObjectType().isMigration()) {
log.error("Script {} is not a migration script.", parentScript);
return null;
}
List<MigrationScript> migrationScripts = new ArrayList<>();
for(Migration migration: migrations) {
MigrationScript script = ScriptFactory.getMigrationScript(parentScript, migration);
if(migrationScripts.contains(script)) {
log.error("Duplicate version {} for script {} found.", script.getVersion(), script);
throw new RuntimeException("Duplicate version number is not allowed in the same script file.");
}
migrationScripts.add(script);
}
return migrationScripts;
}

public static TestScript getTestScript(String scriptPath, String objectName, String content, Script mainScript) {
if (mainScript instanceof SchemaScript) {
SchemaScript schemaScript = (SchemaScript) mainScript;
return new TestScript(scriptPath, objectName, schemaScript.getObjectType(), content, schemaScript);
}
return null;
}
}
Loading