Merge branch 'master' into METAMODEL-1159-composite-query
diff --git a/core/src/main/java/org/apache/metamodel/membrane/app/DataContextSupplier.java b/core/src/main/java/org/apache/metamodel/membrane/app/DataContextSupplier.java
deleted file mode 100644
index 7ce0cc0..0000000
--- a/core/src/main/java/org/apache/metamodel/membrane/app/DataContextSupplier.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.metamodel.membrane.app;
-
-import java.util.function.Supplier;
-
-import org.apache.metamodel.DataContext;
-import org.apache.metamodel.factory.DataContextFactoryRegistryImpl;
-import org.apache.metamodel.factory.DataContextProperties;
-
-public class DataContextSupplier implements Supplier<DataContext> {
-
-    private final String dataSourceName;
-    private final DataContextProperties dataContextProperties;
-
-    public DataContextSupplier(String dataSourceName, DataContextProperties dataContextProperties) {
-        this.dataSourceName = dataSourceName;
-        this.dataContextProperties = dataContextProperties;
-    }
-
-    @Override
-    public DataContext get() {
-        final DataContext dataContext = DataContextFactoryRegistryImpl.getDefaultInstance().createDataContext(
-                dataContextProperties);
-        return dataContext;
-    }
-
-    @Override
-    public String toString() {
-        return "DataContextSupplier[" + dataSourceName + "]";
-    }
-}
diff --git a/core/src/main/java/org/apache/metamodel/membrane/app/federation/FederatedDataContextFactory.java b/core/src/main/java/org/apache/metamodel/membrane/app/federation/FederatedDataContextFactory.java
new file mode 100644
index 0000000..db70942
--- /dev/null
+++ b/core/src/main/java/org/apache/metamodel/membrane/app/federation/FederatedDataContextFactory.java
@@ -0,0 +1,92 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.metamodel.membrane.app.federation;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.apache.metamodel.CompositeDataContext;
+import org.apache.metamodel.ConnectionException;
+import org.apache.metamodel.DataContext;
+import org.apache.metamodel.factory.DataContextFactory;
+import org.apache.metamodel.factory.DataContextProperties;
+import org.apache.metamodel.factory.ResourceFactoryRegistry;
+import org.apache.metamodel.factory.UnsupportedDataContextPropertiesException;
+import org.apache.metamodel.membrane.app.registry.DataSourceRegistry;
+import org.apache.metamodel.membrane.app.registry.TenantContext;
+
+public class FederatedDataContextFactory implements DataContextFactory {
+
+    public static final String DATA_CONTEXT_TYPE = "federated";
+    public static final String PROPERTY_DATA_SOURCES = "datasources";
+
+    private final TenantContext tenant;
+
+    public FederatedDataContextFactory(TenantContext tenant) {
+        this.tenant = tenant;
+    }
+
+    @Override
+    public boolean accepts(DataContextProperties properties, ResourceFactoryRegistry resourceFactoryRegistry) {
+        return DATA_CONTEXT_TYPE.equals(properties.getDataContextType());
+    }
+
+    @Override
+    public DataContext create(DataContextProperties properties, ResourceFactoryRegistry resourceFactoryRegistry)
+            throws UnsupportedDataContextPropertiesException, ConnectionException {
+
+        final Object dataSourcesPropertyValue = properties.toMap().get(PROPERTY_DATA_SOURCES);
+        final Collection<String> dataSourceNames = toDataSourceNames(dataSourcesPropertyValue);
+        final DataSourceRegistry dataSourceRegistry = tenant.getDataSourceRegistry();
+        final List<DataContext> dataContexts =
+                dataSourceNames.stream().map(dataSourceRegistry::openDataContext).collect(Collectors.toList());
+        return new CompositeDataContext(dataContexts);
+    }
+
+    @SuppressWarnings("unchecked")
+    private Collection<String> toDataSourceNames(Object dataSourcesPropertyValue) {
+        if (dataSourcesPropertyValue == null) {
+            return Collections.emptyList();
+        }
+        if ("*".equals(dataSourcesPropertyValue)) {
+            return tenant.getDataSourceRegistry().getDataSourceNames();
+        }
+        if (dataSourcesPropertyValue instanceof String) {
+            final String str = (String) dataSourcesPropertyValue;
+            return Collections.singleton(str);
+        }
+        if (dataSourcesPropertyValue instanceof Collection) {
+            return new HashSet<>((Collection<String>)dataSourcesPropertyValue);
+        }
+        if (dataSourcesPropertyValue.getClass().isArray()) {
+            final Set<String> result = new HashSet<>();
+            final Object[] arr = (Object[]) dataSourcesPropertyValue;
+            for (Object item : arr) {
+                result.addAll(toDataSourceNames(item));
+            }
+            return result;
+        }
+        throw new IllegalArgumentException("Bad '" + PROPERTY_DATA_SOURCES + "' value: " + dataSourcesPropertyValue);
+    }
+
+}
diff --git a/core/src/main/java/org/apache/metamodel/membrane/app/registry/AbstractDataSourceRegistry.java b/core/src/main/java/org/apache/metamodel/membrane/app/registry/AbstractDataSourceRegistry.java
new file mode 100644
index 0000000..b83a509
--- /dev/null
+++ b/core/src/main/java/org/apache/metamodel/membrane/app/registry/AbstractDataSourceRegistry.java
@@ -0,0 +1,45 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.metamodel.membrane.app.registry;
+
+import org.apache.metamodel.DataContext;
+import org.apache.metamodel.factory.DataContextProperties;
+
+public abstract class AbstractDataSourceRegistry implements DataSourceRegistry {
+
+    private final TenantContext tenantContext;
+
+    public AbstractDataSourceRegistry(TenantContext tenantContext) {
+        this.tenantContext = tenantContext;
+    }
+
+    protected DataContextSupplier createDataContextSupplier(String name, DataContextProperties properties) {
+        final DataContextSupplier supplier = new DataContextSupplier(tenantContext, name, properties);
+        return supplier;
+    }
+
+    @Override
+    public DataContext openDataContext(DataContextProperties properties) {
+        return createDataContextSupplier(null, properties).get();
+    }
+
+    protected TenantContext getTenantContext() {
+        return tenantContext;
+    }
+}
diff --git a/core/src/main/java/org/apache/metamodel/membrane/app/registry/DataContextSupplier.java b/core/src/main/java/org/apache/metamodel/membrane/app/registry/DataContextSupplier.java
new file mode 100644
index 0000000..2297dca
--- /dev/null
+++ b/core/src/main/java/org/apache/metamodel/membrane/app/registry/DataContextSupplier.java
@@ -0,0 +1,74 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.metamodel.membrane.app.registry;
+
+import java.util.Collection;
+import java.util.function.Supplier;
+
+import org.apache.metamodel.DataContext;
+import org.apache.metamodel.factory.DataContextFactory;
+import org.apache.metamodel.factory.DataContextFactoryRegistry;
+import org.apache.metamodel.factory.DataContextFactoryRegistryImpl;
+import org.apache.metamodel.factory.DataContextProperties;
+import org.apache.metamodel.factory.ResourceFactoryRegistry;
+import org.apache.metamodel.factory.ResourceFactoryRegistryImpl;
+import org.apache.metamodel.membrane.app.federation.FederatedDataContextFactory;
+
+public class DataContextSupplier implements Supplier<DataContext> {
+
+    private final String dataSourceName;
+    private final DataContextProperties dataContextProperties;
+    private final TenantContext tenantContext;
+
+    public DataContextSupplier(TenantContext tenantContext, String dataSourceName,
+            DataContextProperties dataContextProperties) {
+        this.tenantContext = tenantContext;
+        this.dataSourceName = dataSourceName;
+        this.dataContextProperties = dataContextProperties;
+    }
+
+    @Override
+    public DataContext get() {
+        final DataContextFactoryRegistry registry = getRegistryForTenant();
+        final DataContext dataContext = registry.createDataContext(dataContextProperties);
+        return dataContext;
+    }
+
+    private DataContextFactoryRegistry getRegistryForTenant() {
+        final ResourceFactoryRegistry resourceFactoryRegistry = ResourceFactoryRegistryImpl.getDefaultInstance();
+        final DataContextFactoryRegistry registry = new DataContextFactoryRegistryImpl(resourceFactoryRegistry);
+
+        // Add default/standard factories. This is pretty cumbersome. New constructor/cloning options in MetaModel
+        // should make this easier: https://github.com/apache/metamodel/pull/192
+        final DataContextFactoryRegistry defaultRegistry = DataContextFactoryRegistryImpl.getDefaultInstance();
+        final Collection<DataContextFactory> defaultFactories = defaultRegistry.getFactories();
+        for (DataContextFactory factory : defaultFactories) {
+            registry.addFactory(factory);
+        }
+
+        // add tenant-specific factories
+        registry.addFactory(new FederatedDataContextFactory(tenantContext));
+        return registry;
+    }
+
+    @Override
+    public String toString() {
+        return "DataContextSupplier[" + dataSourceName + "]";
+    }
+}
diff --git a/core/src/main/java/org/apache/metamodel/membrane/app/registry/file/FileBasedDataSourceRegistry.java b/core/src/main/java/org/apache/metamodel/membrane/app/registry/file/FileBasedDataSourceRegistry.java
index 60c77ae..b7a2d31 100644
--- a/core/src/main/java/org/apache/metamodel/membrane/app/registry/file/FileBasedDataSourceRegistry.java
+++ b/core/src/main/java/org/apache/metamodel/membrane/app/registry/file/FileBasedDataSourceRegistry.java
@@ -28,17 +28,18 @@
 
 import org.apache.metamodel.DataContext;
 import org.apache.metamodel.factory.DataContextProperties;
-import org.apache.metamodel.membrane.app.DataContextSupplier;
 import org.apache.metamodel.membrane.app.config.JacksonConfig;
 import org.apache.metamodel.membrane.app.exceptions.DataSourceAlreadyExistException;
 import org.apache.metamodel.membrane.app.exceptions.NoSuchDataSourceException;
-import org.apache.metamodel.membrane.app.registry.DataSourceRegistry;
+import org.apache.metamodel.membrane.app.registry.AbstractDataSourceRegistry;
+import org.apache.metamodel.membrane.app.registry.DataContextSupplier;
+import org.apache.metamodel.membrane.app.registry.TenantContext;
 import org.apache.metamodel.membrane.controllers.model.RestDataSourceDefinition;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.base.Strings;
 
-public class FileBasedDataSourceRegistry implements DataSourceRegistry {
+public class FileBasedDataSourceRegistry extends AbstractDataSourceRegistry {
 
     private static final ObjectMapper OBJECT_MAPPER = JacksonConfig.getObjectMapper();
     private static final String DATASOURCE_FILE_SUFFIX = ".json";
@@ -46,7 +47,8 @@
 
     private final File directory;
 
-    public FileBasedDataSourceRegistry(File directory) {
+    public FileBasedDataSourceRegistry(TenantContext tenantContext, File directory) {
+        super(tenantContext);
         this.directory = directory;
     }
 
@@ -115,7 +117,7 @@
         }
 
         final DataContextSupplier supplier =
-                new DataContextSupplier(dataSourceName, dataSource.toDataContextProperties());
+                createDataContextSupplier(dataSourceName, dataSource.toDataContextProperties());
         return supplier.get();
     }
 
@@ -134,9 +136,4 @@
             throw new UncheckedIOException(new IOException("Unable to delete file: " + file));
         }
     }
-
-    public DataContext openDataContext(DataContextProperties properties) {
-        final DataContextSupplier supplier = new DataContextSupplier(null, properties);
-        return supplier.get();
-    }
 }
diff --git a/core/src/main/java/org/apache/metamodel/membrane/app/registry/file/FileBasedTenantContext.java b/core/src/main/java/org/apache/metamodel/membrane/app/registry/file/FileBasedTenantContext.java
index d830125..7e64d3d 100644
--- a/core/src/main/java/org/apache/metamodel/membrane/app/registry/file/FileBasedTenantContext.java
+++ b/core/src/main/java/org/apache/metamodel/membrane/app/registry/file/FileBasedTenantContext.java
@@ -31,7 +31,8 @@
 
     public FileBasedTenantContext(File directory) {
         this.directory = directory;
-        this.dataContextRegistry = new CachedDataSourceRegistryWrapper(new FileBasedDataSourceRegistry(directory));
+        this.dataContextRegistry =
+                new CachedDataSourceRegistryWrapper(new FileBasedDataSourceRegistry(this, directory));
     }
 
     @Override
diff --git a/core/src/main/java/org/apache/metamodel/membrane/app/registry/memory/InMemoryDataSourceRegistry.java b/core/src/main/java/org/apache/metamodel/membrane/app/registry/memory/InMemoryDataSourceRegistry.java
index a457ec0..bff3a73 100644
--- a/core/src/main/java/org/apache/metamodel/membrane/app/registry/memory/InMemoryDataSourceRegistry.java
+++ b/core/src/main/java/org/apache/metamodel/membrane/app/registry/memory/InMemoryDataSourceRegistry.java
@@ -26,16 +26,17 @@
 
 import org.apache.metamodel.DataContext;
 import org.apache.metamodel.factory.DataContextProperties;
-import org.apache.metamodel.membrane.app.DataContextSupplier;
 import org.apache.metamodel.membrane.app.exceptions.DataSourceAlreadyExistException;
 import org.apache.metamodel.membrane.app.exceptions.NoSuchDataSourceException;
-import org.apache.metamodel.membrane.app.registry.DataSourceRegistry;
+import org.apache.metamodel.membrane.app.registry.AbstractDataSourceRegistry;
+import org.apache.metamodel.membrane.app.registry.TenantContext;
 
-public class InMemoryDataSourceRegistry implements DataSourceRegistry {
+public class InMemoryDataSourceRegistry extends AbstractDataSourceRegistry {
 
     private final Map<String, Supplier<DataContext>> dataSources;
 
-    public InMemoryDataSourceRegistry() {
+    public InMemoryDataSourceRegistry(final TenantContext tenantContext) {
+        super(tenantContext);
         dataSources = new LinkedHashMap<>();
     }
 
@@ -46,7 +47,7 @@
             throw new DataSourceAlreadyExistException(name);
         }
 
-        dataSources.put(name, new DataContextSupplier(name, dataContextProperties));
+        dataSources.put(name, createDataContextSupplier(name, dataContextProperties));
         return name;
     }
 
@@ -71,9 +72,4 @@
         }
         dataSources.remove(dataSourceName);
     }
-
-    public DataContext openDataContext(DataContextProperties properties) {
-        final DataContextSupplier supplier = new DataContextSupplier(null, properties);
-        return supplier.get();
-    }
 }
diff --git a/core/src/main/java/org/apache/metamodel/membrane/app/registry/memory/InMemoryTenantContext.java b/core/src/main/java/org/apache/metamodel/membrane/app/registry/memory/InMemoryTenantContext.java
index 08b2fa9..330f3b4 100644
--- a/core/src/main/java/org/apache/metamodel/membrane/app/registry/memory/InMemoryTenantContext.java
+++ b/core/src/main/java/org/apache/metamodel/membrane/app/registry/memory/InMemoryTenantContext.java
@@ -29,7 +29,7 @@
 
     public InMemoryTenantContext(String tenantIdentifier) {
         this.tenantIdentifier = tenantIdentifier;
-        this.dataContextRegistry = new CachedDataSourceRegistryWrapper(new InMemoryDataSourceRegistry());
+        this.dataContextRegistry = new CachedDataSourceRegistryWrapper(new InMemoryDataSourceRegistry(this));
     }
 
     @Override
diff --git a/postman-tests/Membrane.postman_collection.json b/postman-tests/Membrane.postman_collection.json
index e249ead..78a8f29 100644
--- a/postman-tests/Membrane.postman_collection.json
+++ b/postman-tests/Membrane.postman_collection.json
@@ -1,10 +1,8 @@
 {
-	"variables": [],
 	"info": {
+		"_postman_id": "b07b47aa-5a8c-4be5-bbc3-0fd98040ee92",
 		"name": "Membrane",
-		"_postman_id": "fc875392-a36c-6ff7-46fb-53eab9cd3c3e",
-		"description": "",
-		"schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json"
+		"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
 	},
 	"item": [
 		{
@@ -24,14 +22,21 @@
 				}
 			],
 			"request": {
-				"url": "{{baseUrl}}/swagger.json",
 				"method": "GET",
 				"header": [],
 				"body": {
 					"mode": "raw",
 					"raw": ""
 				},
-				"description": ""
+				"url": {
+					"raw": "{{baseUrl}}/swagger.json",
+					"host": [
+						"{{baseUrl}}"
+					],
+					"path": [
+						"swagger.json"
+					]
+				}
 			},
 			"response": []
 		},
@@ -58,20 +63,26 @@
 				}
 			],
 			"request": {
-				"url": "{{baseUrl}}/my-tenant",
 				"method": "PUT",
 				"header": [
 					{
 						"key": "Content-Type",
-						"value": "application/json",
-						"description": ""
+						"value": "application/json"
 					}
 				],
 				"body": {
 					"mode": "raw",
 					"raw": "{\n    \n}"
 				},
-				"description": ""
+				"url": {
+					"raw": "{{baseUrl}}/my-tenant",
+					"host": [
+						"{{baseUrl}}"
+					],
+					"path": [
+						"my-tenant"
+					]
+				}
 			},
 			"response": []
 		},
@@ -92,14 +103,21 @@
 				}
 			],
 			"request": {
-				"url": "{{baseUrl}}/{{membrane_tenant}}",
 				"method": "GET",
 				"header": [],
 				"body": {
 					"mode": "raw",
 					"raw": ""
 				},
-				"description": ""
+				"url": {
+					"raw": "{{baseUrl}}/{{membrane_tenant}}",
+					"host": [
+						"{{baseUrl}}"
+					],
+					"path": [
+						"{{membrane_tenant}}"
+					]
+				}
 			},
 			"response": []
 		},
@@ -124,20 +142,27 @@
 				}
 			],
 			"request": {
-				"url": "{{baseUrl}}/{{membrane_tenant}}/example-postgres",
 				"method": "PUT",
 				"header": [
 					{
 						"key": "Content-Type",
-						"value": "application/json",
-						"description": ""
+						"value": "application/json"
 					}
 				],
 				"body": {
 					"mode": "raw",
 					"raw": "{\n    \"type\":\"jdbc\",\n    \"url\": \"jdbc:postgresql://example-postgres/membrane\",\n    \"username\": \"membrane\",\n    \"password\": \"secret\"\n}"
 				},
-				"description": ""
+				"url": {
+					"raw": "{{baseUrl}}/{{membrane_tenant}}/example-postgres",
+					"host": [
+						"{{baseUrl}}"
+					],
+					"path": [
+						"{{membrane_tenant}}",
+						"example-postgres"
+					]
+				}
 			},
 			"response": []
 		},
@@ -162,20 +187,27 @@
 				}
 			],
 			"request": {
-				"url": "{{baseUrl}}/{{membrane_tenant}}/example-couchdb",
 				"method": "PUT",
 				"header": [
 					{
 						"key": "Content-Type",
-						"value": "application/json",
-						"description": ""
+						"value": "application/json"
 					}
 				],
 				"body": {
 					"mode": "raw",
 					"raw": "{\n    \"type\":\"couchdb\",\n    \"hostname\": \"example-couchdb\",\n    \"username\": \"membrane\",\n    \"password\": \"secret\"\n}"
 				},
-				"description": ""
+				"url": {
+					"raw": "{{baseUrl}}/{{membrane_tenant}}/example-couchdb",
+					"host": [
+						"{{baseUrl}}"
+					],
+					"path": [
+						"{{membrane_tenant}}",
+						"example-couchdb"
+					]
+				}
 			},
 			"response": []
 		},
@@ -202,20 +234,27 @@
 				}
 			],
 			"request": {
-				"url": "{{baseUrl}}/{{membrane_tenant}}/example-pojo",
 				"method": "PUT",
 				"header": [
 					{
 						"key": "Content-Type",
-						"value": "application/json",
-						"description": ""
+						"value": "application/json"
 					}
 				],
 				"body": {
 					"mode": "raw",
 					"raw": "{\n    \"type\":\"pojo\",\n    \"table-defs\":\"hello_world (greeting VARCHAR, who VARCHAR); foo (bar INTEGER, baz DATE);\"\n}"
 				},
-				"description": ""
+				"url": {
+					"raw": "{{baseUrl}}/{{membrane_tenant}}/example-pojo",
+					"host": [
+						"{{baseUrl}}"
+					],
+					"path": [
+						"{{membrane_tenant}}",
+						"example-pojo"
+					]
+				}
 			},
 			"response": []
 		},
@@ -246,20 +285,27 @@
 				}
 			],
 			"request": {
-				"url": "{{baseUrl}}/{{membrane_tenant}}/{{membrane_data_source}}",
 				"method": "GET",
 				"header": [
 					{
 						"key": "Content-Type",
-						"value": "application/json",
-						"description": ""
+						"value": "application/json"
 					}
 				],
 				"body": {
 					"mode": "raw",
-					"raw": "{\n    \"type\":\"pojo\",\n    \"table-defs\":\"hello_world (greeting VARCHAR, who VARCHAR); foo (bar INTEGER, baz DATE);\"\n}"
+					"raw": ""
 				},
-				"description": ""
+				"url": {
+					"raw": "{{baseUrl}}/{{membrane_tenant}}/{{membrane_data_source}}",
+					"host": [
+						"{{baseUrl}}"
+					],
+					"path": [
+						"{{membrane_tenant}}",
+						"{{membrane_data_source}}"
+					]
+				}
 			},
 			"response": []
 		},
@@ -288,20 +334,29 @@
 				}
 			],
 			"request": {
-				"url": "{{baseUrl}}/{{membrane_tenant}}/{{membrane_data_source}}/s/{{membrane_schema}}",
 				"method": "GET",
 				"header": [
 					{
 						"key": "Content-Type",
-						"value": "application/json",
-						"description": ""
+						"value": "application/json"
 					}
 				],
 				"body": {
 					"mode": "raw",
-					"raw": "{\n    \"type\":\"pojo\",\n    \"table-defs\":\"hello_world (greeting VARCHAR, who VARCHAR); foo (bar INTEGER, baz DATE);\"\n}"
+					"raw": ""
 				},
-				"description": ""
+				"url": {
+					"raw": "{{baseUrl}}/{{membrane_tenant}}/{{membrane_data_source}}/s/{{membrane_schema}}",
+					"host": [
+						"{{baseUrl}}"
+					],
+					"path": [
+						"{{membrane_tenant}}",
+						"{{membrane_data_source}}",
+						"s",
+						"{{membrane_schema}}"
+					]
+				}
 			},
 			"response": []
 		},
@@ -332,20 +387,31 @@
 				}
 			],
 			"request": {
-				"url": "{{baseUrl}}/{{membrane_tenant}}/{{membrane_data_source}}/s/{{membrane_schema}}/t/{{membrane_table}}",
 				"method": "GET",
 				"header": [
 					{
 						"key": "Content-Type",
-						"value": "application/json",
-						"description": ""
+						"value": "application/json"
 					}
 				],
 				"body": {
 					"mode": "raw",
-					"raw": "{\n    \"type\":\"pojo\",\n    \"table-defs\":\"hello_world (greeting VARCHAR, who VARCHAR); foo (bar INTEGER, baz DATE);\"\n}"
+					"raw": ""
 				},
-				"description": ""
+				"url": {
+					"raw": "{{baseUrl}}/{{membrane_tenant}}/{{membrane_data_source}}/s/{{membrane_schema}}/t/{{membrane_table}}",
+					"host": [
+						"{{baseUrl}}"
+					],
+					"path": [
+						"{{membrane_tenant}}",
+						"{{membrane_data_source}}",
+						"s",
+						"{{membrane_schema}}",
+						"t",
+						"{{membrane_table}}"
+					]
+				}
 			},
 			"response": []
 		},
@@ -367,20 +433,32 @@
 				}
 			],
 			"request": {
-				"url": "{{baseUrl}}/{{membrane_tenant}}/{{membrane_data_source}}/s/{{membrane_schema}}/t/{{membrane_table}}/d",
 				"method": "POST",
 				"header": [
 					{
 						"key": "Content-Type",
-						"value": "application/json",
-						"description": ""
+						"value": "application/json"
 					}
 				],
 				"body": {
 					"mode": "raw",
 					"raw": "{\n\t\"insert\": [\n\t\t{\n\t\t\t\"bar\": 42,\n\t\t\t\"baz\": null\n\t\t}\n\t]\n}"
 				},
-				"description": ""
+				"url": {
+					"raw": "{{baseUrl}}/{{membrane_tenant}}/{{membrane_data_source}}/s/{{membrane_schema}}/t/{{membrane_table}}/d",
+					"host": [
+						"{{baseUrl}}"
+					],
+					"path": [
+						"{{membrane_tenant}}",
+						"{{membrane_data_source}}",
+						"s",
+						"{{membrane_schema}}",
+						"t",
+						"{{membrane_table}}",
+						"d"
+					]
+				}
 			},
 			"response": []
 		},
@@ -402,20 +480,32 @@
 				}
 			],
 			"request": {
-				"url": "{{baseUrl}}/{{membrane_tenant}}/{{membrane_data_source}}/s/{{membrane_schema}}/t/{{membrane_table}}/d",
 				"method": "GET",
 				"header": [
 					{
 						"key": "Content-Type",
-						"value": "application/json",
-						"description": ""
+						"value": "application/json"
 					}
 				],
 				"body": {
 					"mode": "raw",
 					"raw": ""
 				},
-				"description": ""
+				"url": {
+					"raw": "{{baseUrl}}/{{membrane_tenant}}/{{membrane_data_source}}/s/{{membrane_schema}}/t/{{membrane_table}}/d",
+					"host": [
+						"{{baseUrl}}"
+					],
+					"path": [
+						"{{membrane_tenant}}",
+						"{{membrane_data_source}}",
+						"s",
+						"{{membrane_schema}}",
+						"t",
+						"{{membrane_table}}",
+						"d"
+					]
+				}
 			},
 			"response": []
 		},
@@ -437,6 +527,17 @@
 				}
 			],
 			"request": {
+				"method": "GET",
+				"header": [
+					{
+						"key": "Content-Type",
+						"value": "application/json"
+					}
+				],
+				"body": {
+					"mode": "raw",
+					"raw": ""
+				},
 				"url": {
 					"raw": "{{baseUrl}}/{{membrane_tenant}}/{{membrane_data_source}}/q?sql=SELECT * FROM {{membrane_table}}",
 					"host": [
@@ -450,26 +551,10 @@
 					"query": [
 						{
 							"key": "sql",
-							"value": "SELECT * FROM {{membrane_table}}",
-							"equals": true,
-							"description": ""
+							"value": "SELECT * FROM {{membrane_table}}"
 						}
-					],
-					"variable": []
-				},
-				"method": "GET",
-				"header": [
-					{
-						"key": "Content-Type",
-						"value": "application/json",
-						"description": ""
-					}
-				],
-				"body": {
-					"mode": "raw",
-					"raw": ""
-				},
-				"description": ""
+					]
+				}
 			},
 			"response": []
 		},
@@ -491,20 +576,128 @@
 				}
 			],
 			"request": {
-				"url": "{{baseUrl}}/{{membrane_tenant}}/{{membrane_data_source}}/s/{{membrane_schema}}/t/{{membrane_table}}/c/{{membrane_column}}",
 				"method": "GET",
 				"header": [
 					{
 						"key": "Content-Type",
-						"value": "application/json",
-						"description": ""
+						"value": "application/json"
 					}
 				],
 				"body": {
 					"mode": "raw",
-					"raw": "{\n    \"type\":\"pojo\",\n    \"table-defs\":\"hello_world (greeting VARCHAR, who VARCHAR); foo (bar INTEGER, baz DATE);\"\n}"
+					"raw": ""
 				},
-				"description": ""
+				"url": {
+					"raw": "{{baseUrl}}/{{membrane_tenant}}/{{membrane_data_source}}/s/{{membrane_schema}}/t/{{membrane_table}}/c/{{membrane_column}}",
+					"host": [
+						"{{baseUrl}}"
+					],
+					"path": [
+						"{{membrane_tenant}}",
+						"{{membrane_data_source}}",
+						"s",
+						"{{membrane_schema}}",
+						"t",
+						"{{membrane_table}}",
+						"c",
+						"{{membrane_column}}"
+					]
+				}
+			},
+			"response": []
+		},
+		{
+			"name": "Create data source - federated",
+			"event": [
+				{
+					"listen": "test",
+					"script": {
+						"id": "9d1f41d7-12f9-44b0-8730-6e869b22e7d2",
+						"exec": [
+							"tests[\"Status code is 200\"] = responseCode.code === 200;",
+							"",
+							"var jsonData = JSON.parse(responseBody);",
+							"tests[\"type is datasource\"] = jsonData.type === \"datasource\";",
+							"tests[\"is NOT updateable\"] = !jsonData.updateable;",
+							"",
+							"tests[\"has > 3 schemas\"] = jsonData.schemas.length > 3;"
+						],
+						"type": "text/javascript"
+					}
+				}
+			],
+			"request": {
+				"method": "PUT",
+				"header": [
+					{
+						"key": "Content-Type",
+						"value": "application/json"
+					}
+				],
+				"body": {
+					"mode": "raw",
+					"raw": "{\n    \"type\": \"federated\",\n    \"datasources\": [\n        \"example-pojo\",\n        \"example-couchdb\",\n        \"example-postgres\"\n    ]\n}"
+				},
+				"url": {
+					"raw": "{{baseUrl}}/{{membrane_tenant}}/example-federated",
+					"host": [
+						"{{baseUrl}}"
+					],
+					"path": [
+						"{{membrane_tenant}}",
+						"example-federated"
+					]
+				}
+			},
+			"response": []
+		},
+		{
+			"name": "Query federated datasource",
+			"event": [
+				{
+					"listen": "test",
+					"script": {
+						"id": "689cf505-4bca-49b1-95a6-a861b026e31b",
+						"exec": [
+							"tests[\"Status code is 200\"] = responseCode.code === 200;",
+							"",
+							"var jsonData = JSON.parse(responseBody);",
+							"tests[\"type is dataset\"] = jsonData.type === \"dataset\";",
+							""
+						],
+						"type": "text/javascript"
+					}
+				}
+			],
+			"request": {
+				"method": "GET",
+				"header": [
+					{
+						"key": "Content-Type",
+						"value": "application/json"
+					}
+				],
+				"body": {
+					"mode": "raw",
+					"raw": ""
+				},
+				"url": {
+					"raw": "{{baseUrl}}/{{membrane_tenant}}/example-federated/q?sql=SELECT * FROM {{membrane_schema}}.{{membrane_table}}",
+					"host": [
+						"{{baseUrl}}"
+					],
+					"path": [
+						"{{membrane_tenant}}",
+						"example-federated",
+						"q"
+					],
+					"query": [
+						{
+							"key": "sql",
+							"value": "SELECT * FROM {{membrane_schema}}.{{membrane_table}}"
+						}
+					]
+				}
 			},
 			"response": []
 		},
@@ -527,14 +720,22 @@
 				}
 			],
 			"request": {
-				"url": "{{baseUrl}}/{{membrane_tenant}}/{{membrane_data_source}}",
 				"method": "DELETE",
 				"header": [],
 				"body": {
 					"mode": "raw",
 					"raw": ""
 				},
-				"description": ""
+				"url": {
+					"raw": "{{baseUrl}}/{{membrane_tenant}}/{{membrane_data_source}}",
+					"host": [
+						"{{baseUrl}}"
+					],
+					"path": [
+						"{{membrane_tenant}}",
+						"{{membrane_data_source}}"
+					]
+				}
 			},
 			"response": []
 		},
@@ -557,11 +758,21 @@
 				}
 			],
 			"request": {
-				"url": "{{baseUrl}}/{{membrane_tenant}}",
 				"method": "DELETE",
 				"header": [],
-				"body": {},
-				"description": ""
+				"body": {
+					"mode": "raw",
+					"raw": ""
+				},
+				"url": {
+					"raw": "{{baseUrl}}/{{membrane_tenant}}",
+					"host": [
+						"{{baseUrl}}"
+					],
+					"path": [
+						"{{membrane_tenant}}"
+					]
+				}
 			},
 			"response": []
 		}