diff --git a/.github/workflows/ado-net-tests.yml b/.github/workflows/ado-net-tests.yml index c605c80e..de4b0b7b 100644 --- a/.github/workflows/ado-net-tests.yml +++ b/.github/workflows/ado-net-tests.yml @@ -35,7 +35,7 @@ jobs: shell: bash - name: spanner-ado-net-tests working-directory: spanner-ado-net/spanner-ado-net-tests - run: dotnet test --verbosity normal + run: dotnet test --filter "Category!=Integration" --verbosity normal shell: bash - name: spanner-ado-net-specification-tests working-directory: spanner-ado-net/spanner-ado-net-specification-tests @@ -52,3 +52,39 @@ jobs: shell: bash # Docker is only supported on Linux on GitHub Actions if: runner.os == 'Linux' && matrix.os != 'ubuntu-24.04-arm' + + integration-tests: + runs-on: ubuntu-latest + services: + emulator: + image: gcr.io/cloud-spanner-emulator/emulator:latest + ports: + - 9010:9010 + - 9020:9020 + strategy: + matrix: + dotnet-version: ['8.0.x', '9.0.x'] + steps: + - name: Checkout code + uses: actions/checkout@v7 + - name: Install dotnet + uses: actions/setup-dotnet@v5 + with: + dotnet-version: ${{ matrix.dotnet-version }} + - name: Install Go + uses: actions/setup-go@v6 + with: + go-version: '1.26.x' + - name: Checkout go-sql-spanner + uses: actions/checkout@v7 + with: + repository: 'googleapis/go-sql-spanner' + path: spanner-ado-net/spanner-ado-net/go-sql-spanner + - name: Build SpannerLib binaries + run: ./build-binaries.sh true + working-directory: spanner-ado-net/spanner-ado-net + shell: bash + - name: Run Schema Integration Tests + working-directory: spanner-ado-net/spanner-ado-net-tests + run: dotnet test --filter "Category=Integration" --verbosity normal + shell: bash diff --git a/spanner-ado-net/spanner-ado-net-tests/SchemaIntegrationTests.cs b/spanner-ado-net/spanner-ado-net-tests/SchemaIntegrationTests.cs new file mode 100644 index 00000000..45983bd0 --- /dev/null +++ b/spanner-ado-net/spanner-ado-net-tests/SchemaIntegrationTests.cs @@ -0,0 +1,178 @@ +// Copyright 2025 Google LLC +// +// Licensed 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 +// +// https://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. + +using System; +using System.Data; +using System.Data.Common; +using System.Linq; +using System.Net.Sockets; +using System.Threading.Tasks; +using NUnit.Framework; + +namespace Google.Cloud.Spanner.DataProvider.Tests; + +[TestFixture] +[Category("Integration")] +public class SchemaIntegrationTests +{ + private static readonly string ConnectionString = "Host=localhost;Port=9010;Data Source=projects/integration-test/instances/integration-test/databases/integration-test;UsePlainText=true;AutoConfigEmulator=true"; + + [OneTimeSetUp] + public async Task Setup() + { + if (!IsEmulatorRunning()) + { + Assert.Ignore("Spanner emulator is not running on localhost:9010"); + return; + } + + // Open connection (which will auto-create instance and database on the emulator!) + await using var connection = new SpannerConnection(ConnectionString); + await connection.OpenAsync(); + + // Drop existing tables if they exist to start fresh (in case of re-runs on a persistent emulator) + try + { + await using var dropCmd1 = new SpannerCommand("DROP TABLE Albums", connection); + await dropCmd1.ExecuteNonQueryAsync(); + } + catch { } + + try + { + await using var dropCmd2 = new SpannerCommand("DROP TABLE Singers", connection); + await dropCmd2.ExecuteNonQueryAsync(); + } + catch { } + + // Create Singers and Albums tables + await using var createSingers = new SpannerCommand( + @"CREATE TABLE Singers ( + SingerId INT64 NOT NULL, + FirstName STRING(1024), + LastName STRING(1024) + ) PRIMARY KEY (SingerId)", connection); + await createSingers.ExecuteNonQueryAsync(); + + await using var createAlbums = new SpannerCommand( + @"CREATE TABLE Albums ( + SingerId INT64 NOT NULL, + AlbumId INT64 NOT NULL, + AlbumTitle STRING(1024), + CONSTRAINT FK_Albums_Singers FOREIGN KEY(SingerId) REFERENCES Singers(SingerId) + ) PRIMARY KEY (SingerId, AlbumId)", connection); + await createAlbums.ExecuteNonQueryAsync(); + } + + private static bool IsEmulatorRunning() + { + using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + try + { + socket.Connect("localhost", 9010); + return true; + } + catch (SocketException) + { + return false; + } + } + + [Test] + public async Task TablesSchema() + { + await using var connection = new SpannerConnection(ConnectionString); + await connection.OpenAsync(); + + var tables = connection.GetSchema("Tables"); + Assert.That(tables, Is.Not.Null); + var tableNames = tables.Rows.Cast().Select(r => (string)r["TABLE_NAME"]).ToList(); + Assert.That(tableNames, Contains.Item("Singers")); + Assert.That(tableNames, Contains.Item("Albums")); + + // Test with restrictions + var restrictedTables = connection.GetSchema("Tables", [null, null, "Singers"]); + Assert.That(restrictedTables.Rows, Has.Count.EqualTo(1)); + var row = restrictedTables.Rows.Cast().Single(); + Assert.That(row["TABLE_NAME"], Is.EqualTo("Singers")); + Assert.That(row["TABLE_TYPE"], Is.EqualTo("BASE TABLE")); + } + + [Test] + public async Task ColumnsSchema() + { + await using var connection = new SpannerConnection(ConnectionString); + await connection.OpenAsync(); + + var columns = connection.GetSchema("Columns", [null, null, "Singers"]); + Assert.That(columns, Is.Not.Null); + Assert.That(columns.Rows, Has.Count.EqualTo(3)); + + var columnNames = columns.Rows.Cast().Select(r => (string)r["COLUMN_NAME"]).ToList(); + Assert.That(columnNames, Is.EquivalentTo(new[] { "SingerId", "FirstName", "LastName" })); + + // Check specific column details + var singerIdCol = columns.Rows.Cast().Single(r => r["COLUMN_NAME"].Equals("SingerId")); + Assert.That(singerIdCol["IS_NULLABLE"], Is.EqualTo("NO")); + Assert.That(singerIdCol["SPANNER_TYPE"], Is.EqualTo("INT64")); + + var firstNameCol = columns.Rows.Cast().Single(r => r["COLUMN_NAME"].Equals("FirstName")); + Assert.That(firstNameCol["IS_NULLABLE"], Is.EqualTo("YES")); + Assert.That(firstNameCol["SPANNER_TYPE"], Is.EqualTo("STRING(1024)")); + } + + [Test] + public async Task IndexesSchema() + { + await using var connection = new SpannerConnection(ConnectionString); + await connection.OpenAsync(); + + var indexes = connection.GetSchema("Indexes", [null, null, "Singers"]); + Assert.That(indexes, Is.Not.Null); + Assert.That(indexes.Rows, Has.Count.GreaterThanOrEqualTo(1)); + + var pkIndex = indexes.Rows.Cast().Single(r => r["INDEX_NAME"].Equals("PRIMARY_KEY")); + Assert.That(pkIndex["INDEX_TYPE"], Is.EqualTo("PRIMARY_KEY")); + Assert.That(pkIndex["IS_UNIQUE"], Is.True); + } + + [Test] + public async Task IndexColumnsSchema() + { + await using var connection = new SpannerConnection(ConnectionString); + await connection.OpenAsync(); + + var indexCols = connection.GetSchema("IndexColumns", [null, null, "Singers", "PRIMARY_KEY"]); + Assert.That(indexCols, Is.Not.Null); + Assert.That(indexCols.Rows, Has.Count.EqualTo(1)); + + var row = indexCols.Rows.Cast().Single(); + Assert.That(row["COLUMN_NAME"], Is.EqualTo("SingerId")); + Assert.That(row["COLUMN_ORDERING"], Is.EqualTo("ASC")); + } + + [Test] + public async Task ForeignKeysSchema() + { + await using var connection = new SpannerConnection(ConnectionString); + await connection.OpenAsync(); + + var foreignKeys = connection.GetSchema("Foreign Keys", [null, null, "Albums"]); + Assert.That(foreignKeys, Is.Not.Null); + Assert.That(foreignKeys.Rows, Has.Count.EqualTo(1)); + + var row = foreignKeys.Rows.Cast().Single(); + Assert.That(row["CONSTRAINT_NAME"], Is.EqualTo("FK_Albums_Singers")); + } +} diff --git a/spanner-ado-net/spanner-ado-net-tests/SchemaTests.cs b/spanner-ado-net/spanner-ado-net-tests/SchemaTests.cs index 6aa7303a..e0bb16f3 100644 --- a/spanner-ado-net/spanner-ado-net-tests/SchemaTests.cs +++ b/spanner-ado-net/spanner-ado-net-tests/SchemaTests.cs @@ -12,15 +12,97 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System; using System.Data; using System.Data.Common; using System.Text.RegularExpressions; +using Google.Cloud.SpannerLib.MockServer; using TypeCode = Google.Cloud.Spanner.V1.TypeCode; namespace Google.Cloud.Spanner.DataProvider.Tests; public class SchemaTests : AbstractMockServerTests { + [SetUp] + public void SetupSchemaResults() + { + Fixture.SpannerMock.AddOrUpdateStatementResult( + "SELECT TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE FROM INFORMATION_SCHEMA.TABLES WHERE 1=1", + StatementResult.CreateResultSet( + [ + Tuple.Create(TypeCode.String, "TABLE_CATALOG"), + Tuple.Create(TypeCode.String, "TABLE_SCHEMA"), + Tuple.Create(TypeCode.String, "TABLE_NAME"), + Tuple.Create(TypeCode.String, "TABLE_TYPE") + ], + [ + ["", "", "my_table", "BASE TABLE"] + ])); + + Fixture.SpannerMock.AddOrUpdateStatementResult( + "SELECT TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION, COLUMN_DEFAULT, IS_NULLABLE, SPANNER_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE 1=1", + StatementResult.CreateResultSet( + [ + Tuple.Create(TypeCode.String, "TABLE_CATALOG"), + Tuple.Create(TypeCode.String, "TABLE_SCHEMA"), + Tuple.Create(TypeCode.String, "TABLE_NAME"), + Tuple.Create(TypeCode.String, "COLUMN_NAME"), + Tuple.Create(TypeCode.Int64, "ORDINAL_POSITION"), + Tuple.Create(TypeCode.String, "COLUMN_DEFAULT"), + Tuple.Create(TypeCode.String, "IS_NULLABLE"), + Tuple.Create(TypeCode.String, "SPANNER_TYPE") + ], + [ + ["", "", "my_table", "id", 1L, null, "NO", "INT64"] + ])); + + Fixture.SpannerMock.AddOrUpdateStatementResult( + "SELECT TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, INDEX_NAME, INDEX_TYPE, IS_UNIQUE, IS_NULL_FILTERED, INDEX_STATE FROM INFORMATION_SCHEMA.INDEXES WHERE 1=1", + StatementResult.CreateResultSet( + [ + Tuple.Create(TypeCode.String, "TABLE_CATALOG"), + Tuple.Create(TypeCode.String, "TABLE_SCHEMA"), + Tuple.Create(TypeCode.String, "TABLE_NAME"), + Tuple.Create(TypeCode.String, "INDEX_NAME"), + Tuple.Create(TypeCode.String, "INDEX_TYPE"), + Tuple.Create(TypeCode.Bool, "IS_UNIQUE"), + Tuple.Create(TypeCode.Bool, "IS_NULL_FILTERED"), + Tuple.Create(TypeCode.String, "INDEX_STATE") + ], + [ + ["", "", "my_table", "PRIMARY_KEY", "PRIMARY_KEY", true, false, "READY"] + ])); + + Fixture.SpannerMock.AddOrUpdateStatementResult( + "SELECT TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, INDEX_NAME, COLUMN_NAME, ORDINAL_POSITION, COLUMN_ORDERING FROM INFORMATION_SCHEMA.INDEX_COLUMNS WHERE 1=1", + StatementResult.CreateResultSet( + [ + Tuple.Create(TypeCode.String, "TABLE_CATALOG"), + Tuple.Create(TypeCode.String, "TABLE_SCHEMA"), + Tuple.Create(TypeCode.String, "TABLE_NAME"), + Tuple.Create(TypeCode.String, "INDEX_NAME"), + Tuple.Create(TypeCode.String, "COLUMN_NAME"), + Tuple.Create(TypeCode.Int64, "ORDINAL_POSITION"), + Tuple.Create(TypeCode.String, "COLUMN_ORDERING") + ], + [ + ["", "", "my_table", "PRIMARY_KEY", "id", 1L, "ASC"] + ])); + + Fixture.SpannerMock.AddOrUpdateStatementResult( + "SELECT TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_TYPE = 'FOREIGN KEY'", + StatementResult.CreateResultSet( + [ + Tuple.Create(TypeCode.String, "TABLE_CATALOG"), + Tuple.Create(TypeCode.String, "TABLE_SCHEMA"), + Tuple.Create(TypeCode.String, "TABLE_NAME"), + Tuple.Create(TypeCode.String, "CONSTRAINT_NAME") + ], + [ + ["", "", "my_table", "FK_my_table_parent"] + ])); + } + [Test] public async Task MetaDataCollections() { @@ -247,4 +329,140 @@ public async Task ReservedWords() Assert.That(reservedWords.Rows, Has.Count.GreaterThan(0)); } + [Test] + public async Task TablesWithRestrictions() + { + Fixture.SpannerMock.AddOrUpdateStatementResult( + "SELECT TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE FROM INFORMATION_SCHEMA.TABLES WHERE 1=1 AND TABLE_NAME = @p2", + StatementResult.CreateResultSet( + [ + Tuple.Create(TypeCode.String, "TABLE_CATALOG"), + Tuple.Create(TypeCode.String, "TABLE_SCHEMA"), + Tuple.Create(TypeCode.String, "TABLE_NAME"), + Tuple.Create(TypeCode.String, "TABLE_TYPE") + ], + [ + ["", "", "my_table", "BASE TABLE"] + ])); + + await using var conn = await OpenConnectionAsync(); + + var tables = conn.GetSchema("Tables", [null, null, "my_table"]); + Assert.That(tables.Rows, Has.Count.EqualTo(1)); + var row = tables.Rows.Cast().Single(); + Assert.That(row["TABLE_NAME"], Is.EqualTo("my_table")); + } + + [Test] + public async Task ColumnsWithRestrictions() + { + Fixture.SpannerMock.AddOrUpdateStatementResult( + "SELECT TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION, COLUMN_DEFAULT, IS_NULLABLE, SPANNER_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE 1=1 AND TABLE_NAME = @p2 AND COLUMN_NAME = @p3", + StatementResult.CreateResultSet( + [ + Tuple.Create(TypeCode.String, "TABLE_CATALOG"), + Tuple.Create(TypeCode.String, "TABLE_SCHEMA"), + Tuple.Create(TypeCode.String, "TABLE_NAME"), + Tuple.Create(TypeCode.String, "COLUMN_NAME"), + Tuple.Create(TypeCode.Int64, "ORDINAL_POSITION"), + Tuple.Create(TypeCode.String, "COLUMN_DEFAULT"), + Tuple.Create(TypeCode.String, "IS_NULLABLE"), + Tuple.Create(TypeCode.String, "SPANNER_TYPE") + ], + [ + ["", "", "my_table", "id", 1L, null, "NO", "INT64"] + ])); + + await using var conn = await OpenConnectionAsync(); + + var columns = conn.GetSchema("Columns", [null, null, "my_table", "id"]); + Assert.That(columns.Rows, Has.Count.EqualTo(1)); + var row = columns.Rows.Cast().Single(); + Assert.That(row["TABLE_NAME"], Is.EqualTo("my_table")); + Assert.That(row["COLUMN_NAME"], Is.EqualTo("id")); + } + + [Test] + public async Task IndexesWithRestrictions() + { + Fixture.SpannerMock.AddOrUpdateStatementResult( + "SELECT TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, INDEX_NAME, INDEX_TYPE, IS_UNIQUE, IS_NULL_FILTERED, INDEX_STATE FROM INFORMATION_SCHEMA.INDEXES WHERE 1=1 AND TABLE_NAME = @p2 AND INDEX_NAME = @p3", + StatementResult.CreateResultSet( + [ + Tuple.Create(TypeCode.String, "TABLE_CATALOG"), + Tuple.Create(TypeCode.String, "TABLE_SCHEMA"), + Tuple.Create(TypeCode.String, "TABLE_NAME"), + Tuple.Create(TypeCode.String, "INDEX_NAME"), + Tuple.Create(TypeCode.String, "INDEX_TYPE"), + Tuple.Create(TypeCode.Bool, "IS_UNIQUE"), + Tuple.Create(TypeCode.Bool, "IS_NULL_FILTERED"), + Tuple.Create(TypeCode.String, "INDEX_STATE") + ], + [ + ["", "", "my_table", "PRIMARY_KEY", "PRIMARY_KEY", true, false, "READY"] + ])); + + await using var conn = await OpenConnectionAsync(); + + var indexes = conn.GetSchema("Indexes", [null, null, "my_table", "PRIMARY_KEY"]); + Assert.That(indexes.Rows, Has.Count.EqualTo(1)); + var row = indexes.Rows.Cast().Single(); + Assert.That(row["TABLE_NAME"], Is.EqualTo("my_table")); + Assert.That(row["INDEX_NAME"], Is.EqualTo("PRIMARY_KEY")); + } + + [Test] + public async Task IndexColumnsWithRestrictions() + { + Fixture.SpannerMock.AddOrUpdateStatementResult( + "SELECT TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, INDEX_NAME, COLUMN_NAME, ORDINAL_POSITION, COLUMN_ORDERING FROM INFORMATION_SCHEMA.INDEX_COLUMNS WHERE 1=1 AND TABLE_NAME = @p2 AND INDEX_NAME = @p3 AND COLUMN_NAME = @p4", + StatementResult.CreateResultSet( + [ + Tuple.Create(TypeCode.String, "TABLE_CATALOG"), + Tuple.Create(TypeCode.String, "TABLE_SCHEMA"), + Tuple.Create(TypeCode.String, "TABLE_NAME"), + Tuple.Create(TypeCode.String, "INDEX_NAME"), + Tuple.Create(TypeCode.String, "COLUMN_NAME"), + Tuple.Create(TypeCode.Int64, "ORDINAL_POSITION"), + Tuple.Create(TypeCode.String, "COLUMN_ORDERING") + ], + [ + ["", "", "my_table", "PRIMARY_KEY", "id", 1L, "ASC"] + ])); + + await using var conn = await OpenConnectionAsync(); + + var indexColumns = conn.GetSchema("IndexColumns", [null, null, "my_table", "PRIMARY_KEY", "id"]); + Assert.That(indexColumns.Rows, Has.Count.EqualTo(1)); + var row = indexColumns.Rows.Cast().Single(); + Assert.That(row["TABLE_NAME"], Is.EqualTo("my_table")); + Assert.That(row["INDEX_NAME"], Is.EqualTo("PRIMARY_KEY")); + Assert.That(row["COLUMN_NAME"], Is.EqualTo("id")); + } + + [Test] + public async Task ForeignKeysWithRestrictions() + { + Fixture.SpannerMock.AddOrUpdateStatementResult( + "SELECT TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_TYPE = 'FOREIGN KEY' AND TABLE_NAME = @p2 AND CONSTRAINT_NAME = @p3", + StatementResult.CreateResultSet( + [ + Tuple.Create(TypeCode.String, "TABLE_CATALOG"), + Tuple.Create(TypeCode.String, "TABLE_SCHEMA"), + Tuple.Create(TypeCode.String, "TABLE_NAME"), + Tuple.Create(TypeCode.String, "CONSTRAINT_NAME") + ], + [ + ["", "", "my_table", "FK_my_table_parent"] + ])); + + await using var conn = await OpenConnectionAsync(); + + var foreignKeys = conn.GetSchema("Foreign Keys", [null, null, "my_table", "FK_my_table_parent"]); + Assert.That(foreignKeys.Rows, Has.Count.EqualTo(1)); + var row = foreignKeys.Rows.Cast().Single(); + Assert.That(row["TABLE_NAME"], Is.EqualTo("my_table")); + Assert.That(row["CONSTRAINT_NAME"], Is.EqualTo("FK_my_table_parent")); + } + } \ No newline at end of file diff --git a/spanner-ado-net/spanner-ado-net/SpannerSchemaProvider.cs b/spanner-ado-net/spanner-ado-net/SpannerSchemaProvider.cs index 73ecdc32..c8ff7bb1 100644 --- a/spanner-ado-net/spanner-ado-net/SpannerSchemaProvider.cs +++ b/spanner-ado-net/spanner-ado-net/SpannerSchemaProvider.cs @@ -48,6 +48,26 @@ internal DataTable GetSchema(string collectionName, string?[]? restrictionValues { FillRestrictions(dataTable, restrictionValues); } + else if (string.Equals(collectionName, "Tables", StringComparison.OrdinalIgnoreCase)) + { + FillTables(dataTable, restrictionValues); + } + else if (string.Equals(collectionName, "Columns", StringComparison.OrdinalIgnoreCase)) + { + FillColumns(dataTable, restrictionValues); + } + else if (string.Equals(collectionName, "Indexes", StringComparison.OrdinalIgnoreCase)) + { + FillIndexes(dataTable, restrictionValues); + } + else if (string.Equals(collectionName, "IndexColumns", StringComparison.OrdinalIgnoreCase)) + { + FillIndexColumns(dataTable, restrictionValues); + } + else if (string.Equals(collectionName, "Foreign Keys", StringComparison.OrdinalIgnoreCase)) + { + FillForeignKeys(dataTable, restrictionValues); + } else { throw new ArgumentException($"Invalid collection name: '{collectionName}'.", nameof(collectionName)); @@ -72,6 +92,11 @@ private void FillMetaDataCollections(DataTable dataTable, string?[]? restriction dataTable.Rows.Add(DbMetaDataCollectionNames.DataTypes, 0, 0); dataTable.Rows.Add(DbMetaDataCollectionNames.ReservedWords, 0, 0); dataTable.Rows.Add(DbMetaDataCollectionNames.Restrictions, 0, 0); + dataTable.Rows.Add("Tables", 4, 3); + dataTable.Rows.Add("Columns", 4, 4); + dataTable.Rows.Add("Indexes", 4, 4); + dataTable.Rows.Add("IndexColumns", 5, 5); + dataTable.Rows.Add("Foreign Keys", 4, 4); } private void FillDataSourceInformation(DataTable dataTable, string?[]? restrictionValues) @@ -356,4 +381,79 @@ private static void FillReservedWords(DataTable dataTable, string?[]? restrictio dataTable.Rows.Add(word); } } + + private void FillSchemaFromQuery(DataTable dataTable, string query, string[] restrictionColumns, string?[]? restrictionValues) + { + if (connection.State != ConnectionState.Open) + { + throw new InvalidOperationException("The connection must be open to retrieve schema information."); + } + + var sql = query; + var parameters = new System.Collections.Generic.List(); + + if (restrictionValues != null) + { + for (var i = 0; i < Math.Min(restrictionValues.Length, restrictionColumns.Length); i++) + { + var val = restrictionValues[i]; + if (!string.IsNullOrEmpty(val)) + { + var paramName = $"@p{i}"; + sql += $" AND {restrictionColumns[i]} = {paramName}"; + parameters.Add(new SpannerParameter(paramName, val)); + } + } + } + + using var command = connection.CreateCommand(); + command.CommandText = sql; + foreach (var param in parameters) + { + command.Parameters.Add(param); + } + + using var reader = command.ExecuteReader(); + dataTable.Load(reader); + } + + private void FillTables(DataTable dataTable, string?[]? restrictionValues) + { + dataTable.TableName = "Tables"; + var query = "SELECT TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE FROM INFORMATION_SCHEMA.TABLES WHERE 1=1"; + var restrictionColumns = new[] { "TABLE_CATALOG", "TABLE_SCHEMA", "TABLE_NAME", "TABLE_TYPE" }; + FillSchemaFromQuery(dataTable, query, restrictionColumns, restrictionValues); + } + + private void FillColumns(DataTable dataTable, string?[]? restrictionValues) + { + dataTable.TableName = "Columns"; + var query = "SELECT TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION, COLUMN_DEFAULT, IS_NULLABLE, SPANNER_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE 1=1"; + var restrictionColumns = new[] { "TABLE_CATALOG", "TABLE_SCHEMA", "TABLE_NAME", "COLUMN_NAME" }; + FillSchemaFromQuery(dataTable, query, restrictionColumns, restrictionValues); + } + + private void FillIndexes(DataTable dataTable, string?[]? restrictionValues) + { + dataTable.TableName = "Indexes"; + var query = "SELECT TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, INDEX_NAME, INDEX_TYPE, IS_UNIQUE, IS_NULL_FILTERED, INDEX_STATE FROM INFORMATION_SCHEMA.INDEXES WHERE 1=1"; + var restrictionColumns = new[] { "TABLE_CATALOG", "TABLE_SCHEMA", "TABLE_NAME", "INDEX_NAME" }; + FillSchemaFromQuery(dataTable, query, restrictionColumns, restrictionValues); + } + + private void FillIndexColumns(DataTable dataTable, string?[]? restrictionValues) + { + dataTable.TableName = "IndexColumns"; + var query = "SELECT TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, INDEX_NAME, COLUMN_NAME, ORDINAL_POSITION, COLUMN_ORDERING FROM INFORMATION_SCHEMA.INDEX_COLUMNS WHERE 1=1"; + var restrictionColumns = new[] { "TABLE_CATALOG", "TABLE_SCHEMA", "TABLE_NAME", "INDEX_NAME", "COLUMN_NAME" }; + FillSchemaFromQuery(dataTable, query, restrictionColumns, restrictionValues); + } + + private void FillForeignKeys(DataTable dataTable, string?[]? restrictionValues) + { + dataTable.TableName = "Foreign Keys"; + var query = "SELECT TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_TYPE = 'FOREIGN KEY'"; + var restrictionColumns = new[] { "TABLE_CATALOG", "TABLE_SCHEMA", "TABLE_NAME", "CONSTRAINT_NAME" }; + FillSchemaFromQuery(dataTable, query, restrictionColumns, restrictionValues); + } }