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
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,8 @@ target/
Jenkinsfile-app
out/
.vscode/
Snowflake/
*.p8
/Snowflake/
*.p8
*.pem
*.pub
/created_scripts/
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.

2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.0'
testImplementation 'org.junit.platform:junit-platform-launcher:1.13.4'
testImplementation 'org.mockito:mockito-core:5.20.0'
testImplementation 'org.mockito:mockito-junit-jupiter:5.20.0'
}

group = 'com.snowflake'
Expand Down
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 USER_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 SCHEMAS 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 SCHEMAS 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
13 changes: 12 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,18 @@
<version>1.13.4</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.20.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.20.0</version>
<scope>test</scope>
</dependency>

</dependencies>
<build>
Expand Down
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
42 changes: 38 additions & 4 deletions src/main/java/com/snowflake/dlsync/ChangeMangerFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,61 @@
import com.snowflake.dlsync.doa.ScriptRepo;
import com.snowflake.dlsync.doa.ScriptSource;
import com.snowflake.dlsync.parser.ParameterInjector;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;

@Slf4j
public class ChangeMangerFactory {
public static ChangeManager createChangeManger() throws IOException {
public static ChangeManager createChangeManger() throws IOException, SQLException {
ConfigManager configManager = new ConfigManager();
return createChangeManger(configManager);
}

public static ChangeManager createChangeManger(String scriptRoot, String profile) throws IOException {
public static ChangeManager createChangeManger(String scriptRoot, String profile) throws IOException, SQLException {
ConfigManager configManager = new ConfigManager(scriptRoot, profile);
return createChangeManger(configManager);
}

public static ChangeManager createChangeManger(ConfigManager configManager) throws IOException {
public static ChangeManager createChangeManger(ConfigManager configManager) throws IOException, SQLException {
configManager.init();

// Create connection
Connection connection = createConnection(configManager.getConfig().getConnection());

// Create dependencies
ScriptSource scriptSource = new ScriptSource(configManager.getScriptRoot());
ScriptRepo scriptRepo = new ScriptRepo(configManager.getConfig().getConnection());
ScriptRepo scriptRepo = new ScriptRepo(connection, configManager.getConfig().getConnection());
scriptRepo.init();
ParameterInjector parameterInjector = new ParameterInjector(configManager.getScriptParameters());
DependencyExtractor dependencyExtractor = new DependencyExtractor();
DependencyGraph dependencyGraph = new DependencyGraph(dependencyExtractor, configManager.getConfig());

return new ChangeManager(configManager.getConfig(), scriptSource, scriptRepo, dependencyGraph, parameterInjector);
}

/**
* Create a JDBC connection to Snowflake
*/
private static Connection createConnection(Properties connectionProperties) throws SQLException {
String account = connectionProperties.getProperty("account");
if (account == null || account.isEmpty()) {
throw new SQLException("Missing 'account' property in connection configuration");
}

String jdbcUrl = "jdbc:snowflake://" + account + ".snowflakecomputing.com/";
log.debug("Creating Snowflake connection for account: {}", account);


try {
return DriverManager.getConnection(jdbcUrl, connectionProperties);
} catch (SQLException e) {
log.error("Failed to create Snowflake connection: {}", e.getMessage());
throw new SQLException("Unable to connect to Snowflake. Please check your account and connection properties.", e);
}
}
}
Loading
Loading