diff --git a/src/DataStax.AstraDB.DataApi/Admin/AstraDatabasesAdmin.cs b/src/DataStax.AstraDB.DataApi/Admin/AstraDatabasesAdmin.cs index 6b047fa..c93071d 100644 --- a/src/DataStax.AstraDB.DataApi/Admin/AstraDatabasesAdmin.cs +++ b/src/DataStax.AstraDB.DataApi/Admin/AstraDatabasesAdmin.cs @@ -106,17 +106,39 @@ public async Task> ListDatabaseNamesAsync(CommandOptions options) } /// - /// Returns a list of database info objects. + /// Synchronous version of /// - /// A list of DatabaseInfo objects. - /// - /// - /// var databases = admin.ListDatabases(); - /// - /// + /// public List ListDatabases() { - return ListDatabasesAsync(null, true).ResultSync(); + return ListDatabases(null, null); + } + + /// + /// Synchronous version of + /// + /// + public List ListDatabases(CommandOptions options) + { + return ListDatabases(null, options); + } + + /// + /// Synchronous version of + /// + /// + public List ListDatabases(ListDatabaseOptions listOptions) + { + return ListDatabases(listOptions, null); + } + + /// + /// Synchronous version of + /// + /// + public List ListDatabases(ListDatabaseOptions listOptions, CommandOptions options) + { + return ListDatabasesAsync(null, options, true).ResultSync(); } /// @@ -130,44 +152,56 @@ public List ListDatabases() /// public Task> ListDatabasesAsync() { - return ListDatabasesAsync(null, false); + return ListDatabasesAsync(null, null); } /// - /// Returns a list of database info objects using specified command options. + /// Asynchronously returns a list of database info objects using specified command options. /// /// The command options to use. - /// A list of DatabaseInfo objects. + /// A task that resolves to a list of DatabaseInfo objects. /// /// - /// var databases = admin.ListDatabases(options); + /// var databases = await admin.ListDatabasesAsync(options); /// /// - public List ListDatabases(CommandOptions options) + public Task> ListDatabasesAsync(CommandOptions options) { - return ListDatabasesAsync(options, true).ResultSync(); + return ListDatabasesAsync(null, options, false); } /// - /// Asynchronously returns a list of database info objects using specified command options. + /// Asynchronously returns a list of database info objects using specified filtering options /// - /// The command options to use. - /// A task that resolves to a list of DatabaseInfo objects. - /// - /// - /// var databases = await admin.ListDatabasesAsync(options); - /// - /// - public Task> ListDatabasesAsync(CommandOptions options) + /// + /// + public Task> ListDatabasesAsync(ListDatabaseOptions listOptions) { - return ListDatabasesAsync(options, false); + return ListDatabasesAsync(listOptions, null, false); } - internal async Task> ListDatabasesAsync(CommandOptions options, bool runSynchronously) + /// + /// Asynchronously returns a list of database info objects using specified command options and filtering options + /// + /// + /// + /// + public Task> ListDatabasesAsync(ListDatabaseOptions listOptions, CommandOptions options) { + return ListDatabasesAsync(listOptions, options, false); + } + + internal async Task> ListDatabasesAsync(ListDatabaseOptions listOptions, CommandOptions options, bool runSynchronously) + { + if (listOptions == null) + { + listOptions = new ListDatabaseOptions(); + } + var command = CreateCommand() .AddUrlPath("databases") .WithTimeoutManager(new DatabaseAdminTimeoutManager()) + .WithPayload(listOptions) .AddCommandOptions(options); var rawResults = await command.RunAsyncRaw>(HttpMethod.Get, runSynchronously).ConfigureAwait(false); @@ -349,7 +383,7 @@ internal async Task CreateDatabaseAsync(DatabaseCreationOptions var databaseName = creationOptions.Name; Guard.NotNullOrEmpty(databaseName, nameof(databaseName)); - List dbList = await ListDatabasesAsync(commandOptions, runSynchronously).ConfigureAwait(false); + List dbList = await ListDatabasesAsync(null, commandOptions, runSynchronously).ConfigureAwait(false); DatabaseInfo existingDb = dbList.FirstOrDefault(item => databaseName.Equals(item.Name)); @@ -568,7 +602,7 @@ public Task DropDatabaseAsync(Guid dbGuid, CommandOptions options) internal async Task DropDatabaseAsync(string databaseName, CommandOptions options, bool runSynchronously) { Guard.NotNullOrEmpty(databaseName, nameof(databaseName)); - var dbList = await ListDatabasesAsync(options, runSynchronously).ConfigureAwait(false); + var dbList = await ListDatabasesAsync(null, options, runSynchronously).ConfigureAwait(false); var dbInfo = dbList.FirstOrDefault(item => item.Name.Equals(databaseName)); if (dbInfo == null) @@ -604,6 +638,19 @@ internal async Task DropDatabaseAsync(Guid dbGuid, CommandOptions options, return false; } + + /// + /// Returns an IDatabaseAdmin instance for the database at the specified URL. + /// + /// + /// + public IDatabaseAdmin GetDatabaseAdmin(string dbUrl) + { + var database = _client.GetDatabase(dbUrl); + return new DatabaseAdminAstra(database, _client, null); + } + + /// /// Retrieves database information for the specified GUID. /// diff --git a/src/DataStax.AstraDB.DataApi/Admin/DatabaseAdminAstra.cs b/src/DataStax.AstraDB.DataApi/Admin/DatabaseAdminAstra.cs index 224c5f4..0edc2ee 100644 --- a/src/DataStax.AstraDB.DataApi/Admin/DatabaseAdminAstra.cs +++ b/src/DataStax.AstraDB.DataApi/Admin/DatabaseAdminAstra.cs @@ -393,6 +393,7 @@ public Task DropKeyspaceAsync(string keyspace) } /// + /// /// Whether or not to wait for the keyspace to be dropped before returning. /// /// @@ -405,8 +406,8 @@ public Task DropKeyspaceAsync(string keyspace, bool waitForCompletion) } /// + /// /// Optional settings that influence request execution. - /// /// /// await admin.DropKeyspaceAsync("myKeyspace", options); @@ -422,7 +423,9 @@ public Task DropKeyspaceAsync(string keyspace, CommandOptions options) } /// + /// /// Whether or not to wait for the keyspace to be dropped before returning. + /// /// /// /// await admin.DropKeyspaceAsync("myKeyspace", true, options); diff --git a/src/DataStax.AstraDB.DataApi/Admin/DatabaseCreationOptions.cs b/src/DataStax.AstraDB.DataApi/Admin/DatabaseCreationOptions.cs index fba1f37..c327b66 100644 --- a/src/DataStax.AstraDB.DataApi/Admin/DatabaseCreationOptions.cs +++ b/src/DataStax.AstraDB.DataApi/Admin/DatabaseCreationOptions.cs @@ -34,11 +34,11 @@ public class DatabaseCreationOptions public string Keyspace { get; set; } = Database.DefaultKeyspace; [JsonPropertyName("capacityUnits")] - public int CapacityUnits { get; set; } = 1; + internal int CapacityUnits { get; set; } = 1; [JsonPropertyName("tier")] - public string Tier { get; set; } = "serverless"; + internal string Tier { get; set; } = "serverless"; [JsonPropertyName("dbType")] - public string DatabaseType { get; set; } = "vector"; + internal string DatabaseType { get; set; } = "vector"; } \ No newline at end of file diff --git a/src/DataStax.AstraDB.DataApi/Admin/ListDatabaseOptions.cs b/src/DataStax.AstraDB.DataApi/Admin/ListDatabaseOptions.cs new file mode 100644 index 0000000..d63c1c6 --- /dev/null +++ b/src/DataStax.AstraDB.DataApi/Admin/ListDatabaseOptions.cs @@ -0,0 +1,79 @@ +/* + * Copyright DataStax, Inc. + * + * 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 + * + * 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. + */ + +using DataStax.AstraDB.DataApi.Core; +using System.Text.Json.Serialization; + +namespace DataStax.AstraDB.DataApi.Admin; + +/// +/// Options used for ListDatabasesAsync. +/// +public class ListDatabaseOptions +{ + /// + /// Filter databases based on specific states. + /// + [JsonPropertyName("include")] + public QueryDatabaseStates StatesToInclude { get; set; } = QueryDatabaseStates.nonterminated; + + /// + /// Filter databases based on cloud provider. + /// + [JsonPropertyName("cloudProvider")] + public QueryCloudProvider Provider { get; set; } = QueryCloudProvider.ALL; + + /// + /// See . If getting an additional page of data, pass in the id of the last database in the previous page. + /// + [JsonPropertyName("starting_after")] + internal string StartingAfter { get; set; } + + /// + /// Number of items to return "per page". + /// + [JsonPropertyName("limit")] + public int PageSizeLimit = 100; +} + +public enum QueryDatabaseStates +{ + nonterminated, + all, + active, + pending, + preparing, + prepared, + initializing, + parked, + parking, + unparking, + terminating, + terminated, + resizing, + error, + maintenance, + suspended, + suspending +} + +public enum QueryCloudProvider +{ + ALL, + AWS, + GCP, + AZURE +} \ No newline at end of file diff --git a/src/DataStax.AstraDB.DataApi/Collections/Collection.cs b/src/DataStax.AstraDB.DataApi/Collections/Collection.cs index 4604db5..b57420c 100644 --- a/src/DataStax.AstraDB.DataApi/Collections/Collection.cs +++ b/src/DataStax.AstraDB.DataApi/Collections/Collection.cs @@ -112,6 +112,7 @@ public Task> InsertOneAsync(T document) } /// + /// /// public Task> InsertOneAsync(T document, CommandOptions commandOptions) { @@ -181,6 +182,7 @@ public Task> InsertManyAsync(List documents) } /// + /// /// Allows specifying whether the documents should be inserted in order as well as the chunk size. public Task> InsertManyAsync(List documents, InsertManyOptions insertOptions) { @@ -188,6 +190,7 @@ public Task> InsertManyAsync(List documents, } /// + /// /// public Task> InsertManyAsync(List documents, CommandOptions commandOptions) { @@ -195,6 +198,8 @@ public Task> InsertManyAsync(List documents, } /// + /// + /// /// public Task> InsertManyAsync(List documents, InsertManyOptions insertOptions, CommandOptions commandOptions) { @@ -470,6 +475,7 @@ public Task FindOneAsync(DocumentFindOptions findOptions) } /// + /// /// public Task FindOneAsync(DocumentFindOptions findOptions, CommandOptions commandOptions) { @@ -493,6 +499,7 @@ public Task FindOneAsync(Filter filter) } /// + /// /// public Task FindOneAsync(Filter filter, CommandOptions commandOptions) { @@ -500,6 +507,7 @@ public Task FindOneAsync(Filter filter, CommandOptions commandOptions) } /// + /// /// public Task FindOneAsync(Filter filter, DocumentFindOptions findOptions) { @@ -507,6 +515,8 @@ public Task FindOneAsync(Filter filter, DocumentFindOptions findOptions } /// + /// + /// /// public Task FindOneAsync(Filter filter, DocumentFindOptions findOptions, CommandOptions commandOptions) { @@ -553,6 +563,7 @@ public Task FindOneAsync(DocumentFindOptions findOptions) } /// + /// /// public Task FindOneAsync(DocumentFindOptions findOptions, CommandOptions commandOptions) { @@ -560,6 +571,7 @@ public Task FindOneAsync(DocumentFindOptions findOptions, C } /// + /// /// public Task FindOneAsync(Filter filter, CommandOptions commandOptions) { @@ -567,6 +579,7 @@ public Task FindOneAsync(Filter filter, CommandOptions comm } /// + /// /// public Task FindOneAsync(Filter filter, DocumentFindOptions findOptions) { @@ -574,6 +587,8 @@ public Task FindOneAsync(Filter filter, DocumentFindOptions } /// + /// + /// /// public Task FindOneAsync(Filter filter, DocumentFindOptions findOptions, CommandOptions commandOptions) { @@ -652,6 +667,7 @@ public FindEnumerator> Find(CommandOptions commandO } /// + /// /// public FindEnumerator> Find(Filter filter, CommandOptions commandOptions) { @@ -689,6 +705,7 @@ public FindEnumerator> Find(CommandO } /// + /// /// public FindEnumerator> Find(Filter filter, CommandOptions commandOptions) where TResult : class { @@ -760,6 +777,8 @@ public Task FindOneAndUpdateAsync(Filter filter, UpdateBuilder update) /// /// Use the parameter on to specify whether the original or updated document should be returned. /// + /// + /// /// Set Sort, Projection, Upsert options public Task FindOneAndUpdateAsync(Filter filter, UpdateBuilder update, FindOneAndUpdateOptions updateOptions) { @@ -767,6 +786,9 @@ public Task FindOneAndUpdateAsync(Filter filter, UpdateBuilder update, } /// + /// + /// + /// /// public Task FindOneAndUpdateAsync(Filter filter, UpdateBuilder update, FindOneAndUpdateOptions updateOptions, CommandOptions commandOptions) { @@ -812,6 +834,8 @@ public Task FindOneAndUpdateAsync(Filter filter, UpdateBuil } /// + /// + /// /// Set Sort, Projection, Upsert options public Task FindOneAndUpdateAsync(Filter filter, UpdateBuilder update, FindOneAndUpdateOptions updateOptions) { @@ -819,6 +843,9 @@ public Task FindOneAndUpdateAsync(Filter filter, UpdateBuil } /// + /// + /// + /// /// public Task FindOneAndUpdateAsync(Filter filter, UpdateBuilder update, FindOneAndUpdateOptions updateOptions, CommandOptions commandOptions) { @@ -873,6 +900,7 @@ public Task FindOneAndReplaceAsync(T replacement) } /// + /// /// /// /// Use the parameter on to specify whether the original or updated document should be returned. @@ -883,6 +911,8 @@ public Task FindOneAndReplaceAsync(T replacement, ReplaceOptions replaceOp } /// + /// + /// /// public Task FindOneAndReplaceAsync(T replacement, ReplaceOptions replaceOptions, CommandOptions commandOptions) { @@ -928,6 +958,7 @@ public Task FindOneAndReplaceAsync(T replacement) } /// + /// /// public Task FindOneAndReplaceAsync(T replacement, ReplaceOptions replaceOptions) { @@ -935,6 +966,8 @@ public Task FindOneAndReplaceAsync(T replacement, ReplaceOptio } /// + /// + /// /// public Task FindOneAndReplaceAsync(T replacement, ReplaceOptions replaceOptions, CommandOptions commandOptions) { @@ -981,6 +1014,8 @@ public Task FindOneAndReplaceAsync(Filter filter, T replacement) } /// + /// + /// /// public Task FindOneAndReplaceAsync(Filter filter, T replacement, ReplaceOptions replaceOptions) { @@ -988,6 +1023,9 @@ public Task FindOneAndReplaceAsync(Filter filter, T replacement, ReplaceOp } /// + /// + /// + /// /// public Task FindOneAndReplaceAsync(Filter filter, T replacement, ReplaceOptions replaceOptions, CommandOptions commandOptions) { @@ -1151,6 +1189,8 @@ public Task ReplaceOneAsync(Filter filter, T replacement) } /// + /// + /// /// public Task ReplaceOneAsync(Filter filter, T replacement, ReplaceOptions replaceOptions) { @@ -1158,6 +1198,9 @@ public Task ReplaceOneAsync(Filter filter, T replacement, Repla } /// + /// + /// + /// /// public Task ReplaceOneAsync(Filter filter, T replacement, ReplaceOptions replaceOptions, CommandOptions commandOptions) { @@ -1342,6 +1385,7 @@ public Task FindOneAndDeleteAsync(FindOneAndDeleteOptions findOptions) } /// + /// /// public Task FindOneAndDeleteAsync(FindOneAndDeleteOptions findOptions, CommandOptions commandOptions) { @@ -1356,6 +1400,7 @@ public Task FindOneAndDeleteAsync(Filter filter) } /// + /// /// public Task FindOneAndDeleteAsync(Filter filter, CommandOptions commandOptions) { @@ -1363,6 +1408,7 @@ public Task FindOneAndDeleteAsync(Filter filter, CommandOptions commandOpt } /// + /// /// public Task FindOneAndDeleteAsync(Filter filter, FindOneAndDeleteOptions findOptions) { @@ -1370,6 +1416,8 @@ public Task FindOneAndDeleteAsync(Filter filter, FindOneAndDeleteOptions + /// + /// /// public Task FindOneAndDeleteAsync(Filter filter, FindOneAndDeleteOptions findOptions, CommandOptions commandOptions) { @@ -1401,6 +1449,7 @@ public Task FindOneAndDeleteAsync(FindOneAndDeleteOptions f } /// + /// /// public Task FindOneAndDeleteAsync(FindOneAndDeleteOptions findOptions, CommandOptions commandOptions) { @@ -1415,6 +1464,7 @@ public Task FindOneAndDeleteAsync(Filter filter) } /// + /// /// public Task FindOneAndDeleteAsync(Filter filter, CommandOptions commandOptions) { @@ -1422,6 +1472,7 @@ public Task FindOneAndDeleteAsync(Filter filter, CommandOpt } /// + /// /// public Task FindOneAndDeleteAsync(Filter filter, FindOneAndDeleteOptions findOptions) { @@ -1429,6 +1480,8 @@ public Task FindOneAndDeleteAsync(Filter filter, FindOneAnd } /// + /// + /// /// public Task FindOneAndDeleteAsync(Filter filter, FindOneAndDeleteOptions findOptions, CommandOptions commandOptions) { @@ -1521,6 +1574,7 @@ public Task DeleteOneAsync(Filter filter) } /// + /// /// public Task DeleteOneAsync(Filter filter, CommandOptions commandOptions) { @@ -1528,6 +1582,7 @@ public Task DeleteOneAsync(Filter filter, CommandOptions comman } /// + /// /// public Task DeleteOneAsync(DeleteOptions deleteOptions, CommandOptions commandOptions) { @@ -1535,6 +1590,7 @@ public Task DeleteOneAsync(DeleteOptions deleteOptions, Command } /// + /// /// public Task DeleteOneAsync(Filter filter, DeleteOptions deleteOptions) { @@ -1542,6 +1598,8 @@ public Task DeleteOneAsync(Filter filter, DeleteOptions dele } /// + /// + /// /// public Task DeleteOneAsync(Filter filter, DeleteOptions deleteOptions, CommandOptions commandOptions) { @@ -1595,6 +1653,7 @@ public Task DeleteManyAsync(Filter filter) } /// + /// /// public Task DeleteManyAsync(Filter filter, CommandOptions commandOptions) { @@ -1700,6 +1759,8 @@ public Task UpdateOneAsync(UpdateBuilder update, UpdateOneOptio } /// + /// + /// /// public Task UpdateOneAsync(UpdateBuilder update, UpdateOneOptions updateOptions, CommandOptions commandOptions) { @@ -1720,6 +1781,8 @@ public Task UpdateOneAsync(Filter filter, UpdateBuilder upda } /// + /// + /// /// public Task UpdateOneAsync(Filter filter, UpdateBuilder update, UpdateOneOptions updateOptions) { @@ -1727,6 +1790,9 @@ public Task UpdateOneAsync(Filter filter, UpdateBuilder upda } /// + /// + /// + /// /// public Task UpdateOneAsync(Filter filter, UpdateBuilder update, UpdateOneOptions updateOptions, CommandOptions commandOptions) { @@ -1782,6 +1848,8 @@ public Task UpdateManyAsync(Filter filter, UpdateBuilder upd } /// + /// + /// /// public Task UpdateManyAsync(Filter filter, UpdateBuilder update, UpdateManyOptions updateOptions) { @@ -1789,6 +1857,9 @@ public Task UpdateManyAsync(Filter filter, UpdateBuilder upd } /// + /// + /// + /// /// public Task UpdateManyAsync(Filter filter, UpdateBuilder update, UpdateManyOptions updateOptions, CommandOptions commandOptions) { @@ -1927,6 +1998,7 @@ public Task CountDocumentsAsync(Filter filter, int maxDocumentsToCount) } /// + /// /// public Task CountDocumentsAsync(Filter filter, CommandOptions commandOptions) { diff --git a/src/DataStax.AstraDB.DataApi/Core/Builders.cs b/src/DataStax.AstraDB.DataApi/Core/Builders.cs index 04a32ff..a1eaa0a 100644 --- a/src/DataStax.AstraDB.DataApi/Core/Builders.cs +++ b/src/DataStax.AstraDB.DataApi/Core/Builders.cs @@ -15,9 +15,28 @@ */ using DataStax.AstraDB.DataApi.Core.Query; +using DataStax.AstraDB.DataApi.Tables; namespace DataStax.AstraDB.DataApi.Core; +/// +/// A collection of builders for interacting with tables and collections +/// +/// +public class Builders +{ + /// + /// A builder for creating table indexes + /// + /// + /// var index = Builders.TableIndex.Text(); + /// table.CreateTextIndex("index_name", r => r.SomeTextProperty, index); + /// + /// + /// + public static TableIndexBuilder TableIndex => new(); +} + /// /// A collection of builders for creating filter, projection, sort, and update definitions /// @@ -74,4 +93,15 @@ public class Builders /// /// public static UpdateBuilder Update => new(); + + /// + /// A builder for creating table indexes + /// + /// + /// var index = Builders.TableIndex.Text(); + /// table.CreateTextIndex("index_name", r => r.SomeTextProperty, index); + /// + /// + /// + public static TableIndexBuilder TableIndex => new(); } \ No newline at end of file diff --git a/src/DataStax.AstraDB.DataApi/Core/CollectionDefinition.cs b/src/DataStax.AstraDB.DataApi/Core/CollectionDefinition.cs index 2dd9f83..3bdd99c 100644 --- a/src/DataStax.AstraDB.DataApi/Core/CollectionDefinition.cs +++ b/src/DataStax.AstraDB.DataApi/Core/CollectionDefinition.cs @@ -16,6 +16,7 @@ using DataStax.AstraDB.DataApi.SerDes; using System; +using System.Collections.Generic; using System.Reflection; using System.Text.Json.Serialization; @@ -66,29 +67,49 @@ internal static CollectionDefinition CheckAddDefinitionsFromAttributes(Collec Type type = typeof(T); PropertyInfo idProperty = null; DocumentIdAttribute idAttribute = null; + LexicalOptionsAttribute lexicalAttribute = null; - if (definition.DefaultId == null) + foreach (var property in type.GetProperties()) { - foreach (var property in type.GetProperties()) + var attr = property.GetCustomAttribute(); + if (attr != null) { - var attr = property.GetCustomAttribute(); - if (attr != null) - { - idProperty = property; - idAttribute = attr; - break; - } + idProperty = property; + idAttribute = attr; + } + var checkLexicalAttribute = property.GetCustomAttribute(); + if (checkLexicalAttribute != null) + { + lexicalAttribute = checkLexicalAttribute; } + } - if (idProperty != null) + if (definition.DefaultId == null && idProperty != null) + { + if (idAttribute.DefaultIdType.HasValue) { - if (idAttribute.DefaultIdType.HasValue) + definition.DefaultId = new DefaultIdOptions() { Type = idAttribute.DefaultIdType.Value }; + } + } + + if (definition.Lexical == null && lexicalAttribute != null) + { + definition.Lexical = new LexicalOptions() + { + Analyzer = new AnalyzerOptions() { - definition.DefaultId = new DefaultIdOptions() { Type = idAttribute.DefaultIdType.Value }; + Tokenizer = new TokenizerOptions() + { + Name = lexicalAttribute.TokenizerName, + Arguments = lexicalAttribute.GetArguments() + }, + Filters = lexicalAttribute.Filters != null ? new List(lexicalAttribute.Filters) : new List(), + CharacterFilters = lexicalAttribute.CharacterFilters != null ? new List(lexicalAttribute.CharacterFilters) : new List() } - } + }; } + return definition; } } diff --git a/src/DataStax.AstraDB.DataApi/Core/CommandUrlBuilder.cs b/src/DataStax.AstraDB.DataApi/Core/CommandUrlBuilder.cs index 87ef15b..e838659 100644 --- a/src/DataStax.AstraDB.DataApi/Core/CommandUrlBuilder.cs +++ b/src/DataStax.AstraDB.DataApi/Core/CommandUrlBuilder.cs @@ -14,6 +14,7 @@ * limitations under the License. */ +using DataStax.AstraDB.DataApi.Utils; using System.Linq; namespace DataStax.AstraDB.DataApi.Core; diff --git a/src/DataStax.AstraDB.DataApi/Core/Commands/Command.cs b/src/DataStax.AstraDB.DataApi/Core/Commands/Command.cs index cd538b2..acb878c 100644 --- a/src/DataStax.AstraDB.DataApi/Core/Commands/Command.cs +++ b/src/DataStax.AstraDB.DataApi/Core/Commands/Command.cs @@ -16,6 +16,7 @@ using DataStax.AstraDB.DataApi.Collections; using DataStax.AstraDB.DataApi.SerDes; +using DataStax.AstraDB.DataApi.Utils; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; @@ -163,8 +164,13 @@ internal string Serialize(T input, JsonSerializerOptions serializeOptions = n serializeOptions.Converters.Add(new ByteArrayAsBinaryJsonConverter()); if (commandOptions.SerializeDateAsDollarDate == true) { - serializeOptions.Converters.Add(new DateTimeConverter()); - serializeOptions.Converters.Add(new DateTimeConverter()); + serializeOptions.Converters.Add(new DateTimeAsDollarDateConverter()); + serializeOptions.Converters.Add(new DateTimeAsDollarDateConverter()); + } + else + { + serializeOptions.Converters.Add(new DateTimeConverter()); + serializeOptions.Converters.Add(new DateTimeNullableConverter()); } if (commandOptions.SerializeGuidAsDollarUuid == true) { @@ -205,8 +211,13 @@ internal T Deserialize(string input) } if (commandOptions.SerializeDateAsDollarDate == true) { - deserializeOptions.Converters.Add(new DateTimeConverter()); - deserializeOptions.Converters.Add(new DateTimeConverter()); + deserializeOptions.Converters.Add(new DateTimeAsDollarDateConverter()); + deserializeOptions.Converters.Add(new DateTimeAsDollarDateConverter()); + } + else + { + deserializeOptions.Converters.Add(new DateTimeConverter()); + deserializeOptions.Converters.Add(new DateTimeNullableConverter()); } deserializeOptions.Converters.Add(new IpAddressConverter()); deserializeOptions.Converters.Add(new AnalyzerOptionsConverter()); diff --git a/src/DataStax.AstraDB.DataApi/Core/CreateTypeCommandOptions.cs b/src/DataStax.AstraDB.DataApi/Core/CreateTypeCommandOptions.cs new file mode 100644 index 0000000..e4d9120 --- /dev/null +++ b/src/DataStax.AstraDB.DataApi/Core/CreateTypeCommandOptions.cs @@ -0,0 +1,28 @@ +/* + * Copyright DataStax, Inc. + * + * 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 + * + * 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. + */ + +namespace DataStax.AstraDB.DataApi.Core; + +/// +/// Additional command options for the Database.CreateType commands. +/// +public class CreateTypeCommandOptions : CommandOptions +{ + /// + /// Skip creating the type if one with the same name already exists + /// + public bool SkipIfExists { get; set; } = false; +} \ No newline at end of file diff --git a/src/DataStax.AstraDB.DataApi/Core/Database.cs b/src/DataStax.AstraDB.DataApi/Core/Database.cs index 1ed0161..53bbb7f 100644 --- a/src/DataStax.AstraDB.DataApi/Core/Database.cs +++ b/src/DataStax.AstraDB.DataApi/Core/Database.cs @@ -22,6 +22,9 @@ using DataStax.AstraDB.DataApi.Utils; using System; using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -140,6 +143,7 @@ public Task DoesCollectionExistAsync(string collectionName) } /// + /// /// The options to use for the command, useful for overriding the keyspace. public async Task DoesCollectionExistAsync(string collectionName, DatabaseCommandOptions commandOptions) { @@ -286,6 +290,8 @@ public Task> CreateCollectionAsync(string collectionName) } /// + /// + /// /// The options to use for the command, useful for overriding the keyspace. public Task> CreateCollectionAsync(string collectionName, DatabaseCommandOptions commandOptions) { @@ -293,6 +299,7 @@ public Task> CreateCollectionAsync(string collectionName, D } /// + /// /// Specify options to use when creating the collection. public Task> CreateCollectionAsync(string collectionName, CollectionDefinition definition) { @@ -300,6 +307,8 @@ public Task> CreateCollectionAsync(string collectionName, C } /// + /// + /// /// The options to use for the command, useful for overriding the keyspace. public Task> CreateCollectionAsync(string collectionName, CollectionDefinition definition, DatabaseCommandOptions options) { @@ -355,6 +364,7 @@ public Task> CreateCollectionAsync(string collectionName) where } /// + /// /// Specify options to use when creating the collection. public Task> CreateCollectionAsync(string collectionName, CollectionDefinition definition) where T : class { @@ -362,6 +372,7 @@ public Task> CreateCollectionAsync(string collectionName, Colle } /// + /// /// The options to use for the command, useful for overriding the keyspace. public Task> CreateCollectionAsync(string collectionName, DatabaseCommandOptions options) where T : class { @@ -369,6 +380,8 @@ public Task> CreateCollectionAsync(string collectionName, Datab } /// + /// + /// /// The options to use for the command, useful for overriding the keyspace. public Task> CreateCollectionAsync(string collectionName, CollectionDefinition definition, DatabaseCommandOptions options) where T : class { @@ -494,6 +507,7 @@ public Collection GetCollection(string collectionName) } /// + /// /// The options to use for the command, useful for overriding the keyspace. public Collection GetCollection(string collectionName, DatabaseCommandOptions options) { @@ -508,6 +522,7 @@ public Collection GetCollection(string collectionName) where T : class } /// + /// /// The options to use for the command, useful for overriding the keyspace. public Collection GetCollection(string collectionName, DatabaseCommandOptions options) where T : class { @@ -523,6 +538,7 @@ public Collection GetCollection(string collectionName) where T : } /// + /// /// The options to use for the command, useful for overriding the keyspace. public Collection GetCollection(string collectionName, DatabaseCommandOptions options) where T : class { @@ -559,6 +575,7 @@ public Task DropCollectionAsync(string collectionName) } /// + /// /// The options to use for the command, useful for overriding the keyspace public Task DropCollectionAsync(string collectionName, DatabaseCommandOptions options) { @@ -595,10 +612,23 @@ private async Task DropCollectionAsync(string collectionName, DatabaseCommandOpt return CreateTableAsync(tableName, null); } - public Task> CreateTableAsync(string tableName, DatabaseCommandOptions options) where TRow : class, new() + public async Task> CreateTableAsync(string tableName, DatabaseCommandOptions options) where TRow : class, new() { + var udtProperties = TypeUtilities.FindPropertiesWithUserDefinedTypeAttribute(typeof(TRow)); + if (udtProperties.Any()) + { + var existingTypes = await ListTypeNamesAsync(); + foreach (var udtProperty in udtProperties) + { + var typeName = UserDefinedTypeRequest.GetUserDefinedTypeName(udtProperty.UnderlyingType, udtProperty.Attribute); + if (!existingTypes.Contains(typeName)) + { + await CreateTypeAsync(typeName, UserDefinedTypeRequest.CreateDefinitionFromType(udtProperty.UnderlyingType), new CreateTypeCommandOptions() { SkipIfExists = true }); + } + } + } var definition = TableDefinition.CreateTableDefinition(); - return CreateTableAsync(tableName, definition, options, false); + return await CreateTableAsync(tableName, definition, options, false); } public Task> CreateTableAsync(string tableName, TableDefinition definition) @@ -660,6 +690,7 @@ public Table GetTable(string tableName) } /// + /// /// The options to use for the command, useful for overriding the keyspace, for example. public Table GetTable(string tableName, DatabaseCommandOptions options) { @@ -674,6 +705,7 @@ public Table GetTable(string tableName) where T : class } /// + /// /// The options to use for the command, useful for overriding the keyspace, for example. public Table GetTable(string tableName, DatabaseCommandOptions options) where TRow : class { @@ -785,6 +817,7 @@ public Task DropTableAsync(string tableName) } /// + /// /// The options to use for the command, useful for overriding the keyspace, for example. public Task DropTableAsync(string tableName, DatabaseCommandOptions options) { @@ -956,6 +989,51 @@ private async Task> ListTableNamesAsync(DatabaseCommandOptions opti return result.Result.Tables; } + /// + /// Synchronous version of + /// + /// + public void DropTableIndex(Expression> column) + { + var indexName = $"{column.GetMemberNameTree()}_idx"; + DropTableIndex(indexName, null); + } + + /// + /// Synchronous version of + /// + /// + public void DropTableIndex(Expression> column, DropIndexCommandOptions commandOptions) + { + var indexName = $"{column.GetMemberNameTree()}_idx"; + DropTableIndexAsync(indexName, commandOptions, true).ResultSync(); + } + + /// + /// Drops an index on the table. + /// + /// The column to drop the index from + /// + /// Index name will be generated as "{columnName}_idx". Use an overload that accepts an index name if the index was created with a custom name. + /// + public Task DropTableIndexAsync(Expression> column) + { + var indexName = $"{column.GetMemberNameTree()}_idx"; + return DropTableIndexAsync(indexName, null, false); + } + + /// + /// The column to drop the index from + /// + /// + /// Index name will be generated as "{columnName}_idx". Use an overload that accepts an index name if the index was created with a custom name. + /// + public Task DropTableIndexAsync(Expression> column, DropIndexCommandOptions commandOptions) + { + var indexName = $"{column.GetMemberNameTree()}_idx"; + return DropTableIndexAsync(indexName, commandOptions, false); + } + /// /// Synchronous version of @@ -985,6 +1063,7 @@ public Task DropTableIndexAsync(string indexName) } /// + /// /// public Task DropTableIndexAsync(string indexName, DropIndexCommandOptions commandOptions) { @@ -1008,6 +1087,390 @@ private async Task DropTableIndexAsync(string indexName, DropIndexCommandOptions await command.RunAsyncReturnStatus>(runSynchronously).ConfigureAwait(false); } + /// + /// Synchronous version of + /// + /// + public void CreateType() where T : new() + { + CreateType(null as CreateTypeCommandOptions); + } + + /// + /// Synchronous version of + /// + /// + public void CreateType(string typeName) where T : new() + { + CreateType(typeName, null); + } + + /// + /// Synchronous version of + /// + /// + public void CreateType(CreateTypeCommandOptions options) + { + var typeName = UserDefinedTypeRequest.GetUserDefinedTypeName(); + var definition = UserDefinedTypeRequest.CreateDefinitionFromType(); + CreateType(typeName, definition, options); + } + + /// + /// Synchronous version of + /// + /// + public void CreateType(string typeName, CreateTypeCommandOptions options) + { + var definition = UserDefinedTypeRequest.CreateDefinitionFromType(); + CreateType(typeName, definition, options); + } + + /// + /// Synchronous version of + /// + /// + public void CreateType(string typeName, UserDefinedTypeDefinition definition) + { + CreateType(typeName, definition, null); + } + + /// + /// Synchronous version of + /// + /// + public void CreateType(string typeName, UserDefinedTypeDefinition definition, CreateTypeCommandOptions options) + { + CreateTypeAsync(typeName, definition, options, true).ResultSync(); + } + + /// + /// Create a User Defined Type dynamically by specifying the class that defines the type + /// + /// + /// If the class includes a attribute, that name will be used, otherwise the name of the class itself will be used. + /// Columns that do not match available types () will be ignored. + /// If properties include a attribute, that name will be used, otherwise the name of the property itself will be used. + /// + /// + public Task CreateTypeAsync() where T : new() + { + return CreateTypeAsync(null as CreateTypeCommandOptions); + } + + /// + /// + public Task CreateTypeAsync(string typeName) + { + return CreateTypeAsync(typeName, null); + } + + /// + /// + public Task CreateTypeAsync(CreateTypeCommandOptions options) + { + string typeName = UserDefinedTypeRequest.GetUserDefinedTypeName(); + return CreateTypeAsync(typeName, options); + } + + /// + /// + /// + public Task CreateTypeAsync(string typeName, CreateTypeCommandOptions options) + { + var definition = UserDefinedTypeRequest.CreateDefinitionFromType(); + return CreateTypeAsync(typeName, definition, options); + } + + /// + /// Create a User Defined Type given the + /// + /// + /// + public Task CreateTypeAsync(string typeName, UserDefinedTypeDefinition definition) + { + return CreateTypeAsync(typeName, definition, null); + } + + /// + /// + /// + /// + public Task CreateTypeAsync(string typeName, UserDefinedTypeDefinition definition, CreateTypeCommandOptions options) + { + return CreateTypeAsync(typeName, definition, options, false); + } + + private async Task CreateTypeAsync(string typeName, UserDefinedTypeDefinition definition, CreateTypeCommandOptions options, bool runSynchronously) + { + if (options == null) + { + options = new CreateTypeCommandOptions(); + } + var request = new UserDefinedTypeRequest() + { + Name = typeName, + TypeDefinition = definition + }; + request.SetSkipIfExists(options.SkipIfExists); + var command = CreateCommand("createType") + .WithPayload(request) + .WithTimeoutManager(new TableAdminTimeoutManager()) + .AddCommandOptions(options); + await command.RunAsyncReturnStatus>(runSynchronously).ConfigureAwait(false); + } + + /// + /// Synchronous version of + /// + /// + public void DropType() where T : new() + { + DropType(null as DropTypeCommandOptions); + } + + /// + /// Synchronous version of + /// + /// + public void DropType(string typeName) + { + DropType(typeName, null); + } + + /// + /// Synchronous version of + /// + /// + public void DropType(DropTypeCommandOptions options) + { + var typeName = UserDefinedTypeRequest.GetUserDefinedTypeName(); + DropType(typeName, options); + } + + /// + /// Synchronous version of + /// + /// + public void DropType(string typeName, DropTypeCommandOptions options) + { + DropType(typeName, options); + } + + /// + /// Synchronous version of + /// + /// + public void DropType(string typeName, DropTypeCommandOptions options) + { + DropTypeAsync(typeName, options, true).ResultSync(); + } + + /// + /// Drop a User Defined Type dynamically by specifying the class that defines the type + /// + /// + /// If the class includes a attribute, that name will be used, otherwise the name of the class itself will be used. + /// Columns that do not match available types () will be ignored. + /// If properties include a attribute, that name will be used, otherwise the name of the property itself will be used. + /// + /// + public Task DropTypeAsync() where T : new() + { + return DropTypeAsync(null as DropTypeCommandOptions); + } + + /// + /// + public Task DropTypeAsync(string typeName) + { + return DropTypeAsync(typeName, null); + } + + /// + /// + public Task DropTypeAsync(DropTypeCommandOptions options) + { + string typeName = UserDefinedTypeRequest.GetUserDefinedTypeName(); + return DropTypeAsync(typeName, options); + } + + /// + /// + /// + public Task DropTypeAsync(string typeName, DropTypeCommandOptions options) + { + return DropTypeAsync(typeName, options, false); + } + + private async Task DropTypeAsync(string typeName, DropTypeCommandOptions options, bool runSynchronously) + { + if (options == null) + { + options = new DropTypeCommandOptions(); + } + var request = new DropUserDefinedTypeRequest() + { + Name = typeName + }; + request.SetSkipIfExists(options.SkipIfExists); + var command = CreateCommand("dropType") + .WithPayload(request) + .WithTimeoutManager(new TableAdminTimeoutManager()) + .AddCommandOptions(options); + await command.RunAsyncReturnStatus>(runSynchronously).ConfigureAwait(false); + } + + /// + /// Synchronous version of + /// + /// + public void AlterType(AlterUserDefinedTypeDefinition definition) + { + AlterType(definition, null); + } + + /// + /// Synchronous version of + /// + /// + public void AlterType(AlterUserDefinedTypeDefinition definition, CommandOptions options) + { + AlterTypeAsync(definition, options, true).ResultSync(); + } + + + /// + /// Alter a User Defined Type given the + /// + /// The definition of the User Defined Type to alter. + public Task AlterTypeAsync(AlterUserDefinedTypeDefinition definition) + { + return AlterTypeAsync(definition, null); + } + + /// + /// + /// + public Task AlterTypeAsync(AlterUserDefinedTypeDefinition definition, CommandOptions options) + { + return AlterTypeAsync(definition, options, false); + } + + private async Task AlterTypeAsync(AlterUserDefinedTypeDefinition definition, CommandOptions options, bool runSynchronously) + { + if (options == null) + { + options = new CommandOptions(); + } + var command = CreateCommand("alterType") + .WithPayload(definition) + .WithTimeoutManager(new TableAdminTimeoutManager()) + .AddCommandOptions(options); + await command.RunAsyncReturnStatus>(runSynchronously).ConfigureAwait(false); + } + + /// + /// Synchronous version of + /// + /// + public IEnumerable ListTypeNames() + { + return ListTypeNames(null); + } + + /// + /// Synchronous version of + /// + /// + public IEnumerable ListTypeNames(DatabaseCommandOptions options) + { + return ListTypeNamesAsync(options).ResultSync(); + } + + /// + /// List User Defined Types + /// + /// + public Task> ListTypeNamesAsync() + { + return ListTypeNamesAsync(null); + } + + /// + /// List User Defined Type names + /// + /// + /// + public async Task> ListTypeNamesAsync(DatabaseCommandOptions options) + { + var typeInfos = await ListTypesAsync(options, false, false); + return typeInfos.Select(x => x.Name); + } + + /// + /// Synchronous version of + /// + /// + public List ListTypes() + { + return ListTypes(null); + } + + /// + /// Synchronous version of + /// + /// + public List ListTypes(DatabaseCommandOptions options) + { + return ListTypesAsync(options, true, true).ResultSync(); + } + + /// + /// List User Defined Types + /// + /// + public Task> ListTypesAsync() + { + return ListTypesAsync(null); + } + + /// + /// List User Defined Types + /// + /// + /// + public Task> ListTypesAsync(DatabaseCommandOptions options) + { + return ListTypesAsync(options, true, false); + } + + private async Task> ListTypesAsync(DatabaseCommandOptions options, bool includeDetails, bool runSynchronously) + { + var payload = new + { + options = new + { + explain = includeDetails + } + }; + var command = CreateCommand("listTypes") + .WithPayload(payload) + .WithTimeoutManager(new TableAdminTimeoutManager()) + .AddCommandOptions(options); + if (includeDetails) + { + var result = await command.RunAsyncReturnStatus(runSynchronously).ConfigureAwait(false); + return result.Result.Types; + } + else + { + var result = await command.RunAsyncReturnStatus(runSynchronously).ConfigureAwait(false); + return result.Result.Types.Select(name => new UserDefinedTypeInfo { Name = name }).ToList(); + } + } + + internal static Guid? GetDatabaseIdFromUrl(string url) { if (string.IsNullOrWhiteSpace(url)) diff --git a/src/DataStax.AstraDB.DataApi/Core/DropTypeCommandOptions.cs b/src/DataStax.AstraDB.DataApi/Core/DropTypeCommandOptions.cs new file mode 100644 index 0000000..a0201e9 --- /dev/null +++ b/src/DataStax.AstraDB.DataApi/Core/DropTypeCommandOptions.cs @@ -0,0 +1,28 @@ +/* + * Copyright DataStax, Inc. + * + * 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 + * + * 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. + */ + +namespace DataStax.AstraDB.DataApi.Core; + +/// +/// Additional command options for the Database.CreateType commands. +/// +public class DropTypeCommandOptions : CommandOptions +{ + /// + /// Skip creating the type if one with the same name already exists + /// + public bool SkipIfExists { get; set; } = false; +} \ No newline at end of file diff --git a/src/DataStax.AstraDB.DataApi/Core/Extensions.cs b/src/DataStax.AstraDB.DataApi/Core/Extensions.cs deleted file mode 100644 index 9bad811..0000000 --- a/src/DataStax.AstraDB.DataApi/Core/Extensions.cs +++ /dev/null @@ -1,53 +0,0 @@ - -/* - * Copyright DataStax, Inc. - * - * 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 - * - * 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. - */ - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace DataStax.AstraDB.DataApi.Core; - -internal static class CoreExtensions -{ - internal static string ToUrlString(this ApiVersion apiVersion) - { - return apiVersion switch - { - ApiVersion.V1 => "v1", - _ => "v1", - }; - } - - internal static TResult ResultSync(this Task task) - { - return task.GetAwaiter().GetResult(); - } - - internal static void ResultSync(this Task task) - { - task.GetAwaiter().GetResult(); - } - - internal static IEnumerable> CreateBatch(this IEnumerable list, int chunkSize) - { - for (int i = 0; i < list.Count(); i += chunkSize) - { - yield return list.Skip(i).Take(Math.Min(chunkSize, list.Count() - i)); - } - } -} \ No newline at end of file diff --git a/src/DataStax.AstraDB.DataApi/Core/IndexBuilder.cs b/src/DataStax.AstraDB.DataApi/Core/IndexBuilder.cs new file mode 100644 index 0000000..aaf8955 --- /dev/null +++ b/src/DataStax.AstraDB.DataApi/Core/IndexBuilder.cs @@ -0,0 +1,27 @@ +/* + * Copyright DataStax, Inc. + * + * 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 + * + * 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. + */ + +using System; +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace DataStax.AstraDB.DataApi.Core; + + +public class IndexBuilder +{ + +} \ No newline at end of file diff --git a/src/DataStax.AstraDB.DataApi/Core/LexicalOptionsAttribute.cs b/src/DataStax.AstraDB.DataApi/Core/LexicalOptionsAttribute.cs new file mode 100644 index 0000000..0ecf295 --- /dev/null +++ b/src/DataStax.AstraDB.DataApi/Core/LexicalOptionsAttribute.cs @@ -0,0 +1,53 @@ +/* + * Copyright DataStax, Inc. + * + * 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 + * + * 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. + */ + +using System; +using System.Collections.Generic; +using System.Text.Json; + +namespace DataStax.AstraDB.DataApi.Core; + +/// +/// Specifies options for creating lexical analysis based on a property +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] +public class LexicalOptionsAttribute : Attribute +{ + public string TokenizerName { get; set; } = "standard"; + public string[] Filters { get; set; } = new string[0]; + public string[] CharacterFilters { get; set; } = new string[0]; + + public string TokenizerArgumentsJson { get; set; } + + public LexicalOptionsAttribute() { } + + internal Dictionary GetArguments() + { + if (string.IsNullOrEmpty(TokenizerArgumentsJson) || TokenizerArgumentsJson == "{}") + return new Dictionary(); + + try + { + return JsonSerializer.Deserialize>(TokenizerArgumentsJson); + } + catch + { + return new Dictionary(); + } + } +} + + diff --git a/src/DataStax.AstraDB.DataApi/Core/Query/DocumentSortBuilder.cs b/src/DataStax.AstraDB.DataApi/Core/Query/DocumentSortBuilder.cs index 98f4af3..e003a5d 100644 --- a/src/DataStax.AstraDB.DataApi/Core/Query/DocumentSortBuilder.cs +++ b/src/DataStax.AstraDB.DataApi/Core/Query/DocumentSortBuilder.cs @@ -75,6 +75,13 @@ public DocumentSortBuilder Vectorize(string valueToVectorize) return this; } + + public DocumentSortBuilder Lexical(string value) + { + Sorts.Add(Sort.Lexical(value)); + return this; + } + internal new DocumentSortBuilder Clone() { var clone = new DocumentSortBuilder(); diff --git a/src/DataStax.AstraDB.DataApi/Core/Query/FilterBuilder.cs b/src/DataStax.AstraDB.DataApi/Core/Query/FilterBuilder.cs index 34d3757..e2d9a1c 100644 --- a/src/DataStax.AstraDB.DataApi/Core/Query/FilterBuilder.cs +++ b/src/DataStax.AstraDB.DataApi/Core/Query/FilterBuilder.cs @@ -14,6 +14,7 @@ * limitations under the License. */ +using DataStax.AstraDB.DataApi.Core.Commands; using DataStax.AstraDB.DataApi.Utils; using System; using System.Collections.Generic; @@ -353,6 +354,7 @@ public Filter Nin(Expression> expression, TField[] /// /// The type of the field to check /// An expression that represents the field for this filter + /// /// The value to not match /// The filter public Filter Nin(Expression> expression, TField value) @@ -364,6 +366,8 @@ public Filter Nin(Expression> expression, TField va /// Not in operator -- Match documents where the array field does not match the specified value. /// /// The type of the field to check + /// + /// /// The value to not match /// The filter public Filter Nin(string field, object value) @@ -482,6 +486,16 @@ public Filter CompoundKey(PrimaryKeyFilter[] partitionColumns, Filter[] cl return new Filter(null, dictionary); } + /// + /// Lexical match operator -- Matches documents where the document's lexical field value is a exicographical match to the specified string of space-separated keywords or terms + /// + /// + /// + public Filter LexicalMatch(string value) + { + return new Filter(DataApiKeywords.Lexical, FilterOperator.Match, value); + } + } public class PrimaryKeyFilter diff --git a/src/DataStax.AstraDB.DataApi/Core/Query/FilterOperator.cs b/src/DataStax.AstraDB.DataApi/Core/Query/FilterOperator.cs index 86d2654..295025d 100644 --- a/src/DataStax.AstraDB.DataApi/Core/Query/FilterOperator.cs +++ b/src/DataStax.AstraDB.DataApi/Core/Query/FilterOperator.cs @@ -30,4 +30,5 @@ public class FilterOperator public const string All = "$all"; public const string Size = "$size"; public const string Contains = "$contains"; + public const string Match = "$match"; } diff --git a/src/DataStax.AstraDB.DataApi/Core/Query/ProjectionBuilder.cs b/src/DataStax.AstraDB.DataApi/Core/Query/ProjectionBuilder.cs index e2627c8..3653b85 100644 --- a/src/DataStax.AstraDB.DataApi/Core/Query/ProjectionBuilder.cs +++ b/src/DataStax.AstraDB.DataApi/Core/Query/ProjectionBuilder.cs @@ -111,6 +111,8 @@ public ExclusiveProjectionBuilder Exclude(Expression> /// Specify a field to include in the projection. /// /// The name of the field to include. + /// + /// /// The projection builder. public ProjectionBuilder Slice(string fieldName, int start, int length = 0) { @@ -128,6 +130,8 @@ public ProjectionBuilder Slice(string fieldName, int start, int length = 0) /// /// The type of the field to include. /// The field to include in the projection. + /// + /// /// The projection builder. public ProjectionBuilder Slice(Expression> fieldExpression, int start, int length = 0) { @@ -214,6 +218,7 @@ public InclusiveProjectionBuilder ExcludeSpecial(string fieldName) /// /// Specify a special field to exclude from the projection. /// + /// /// The name of the field to exclude. /// The projection builder. public InclusiveProjectionBuilder ExcludeSpecial(Expression> fieldExpression) @@ -244,6 +249,8 @@ public InclusiveProjectionBuilder ExcludeSpecial(Expression /// The name of the field to include. + /// + /// /// The projection builder. public InclusiveProjectionBuilder Slice(string fieldName, int start, int length = 0) { @@ -261,6 +268,8 @@ public InclusiveProjectionBuilder Slice(string fieldName, int start, int leng /// /// The type of the field to include. /// The field to include in the projection. + /// + /// /// The projection builder. public InclusiveProjectionBuilder Slice(Expression> fieldExpression, int start, int length = 0) { @@ -318,6 +327,8 @@ public ExclusiveProjectionBuilder Exclude(Expression> /// Specify a field to include in the projection. /// /// The name of the field to include. + /// + /// /// The projection builder. public ExclusiveProjectionBuilder Slice(string fieldName, int start, int length = 0) { @@ -335,6 +346,8 @@ public ExclusiveProjectionBuilder Slice(string fieldName, int start, int leng /// /// The type of the field to include. /// The field to include in the projection. + /// + /// /// The projection builder. public ExclusiveProjectionBuilder Slice(Expression> fieldExpression, int start, int length = 0) { @@ -362,6 +375,7 @@ public ExclusiveProjectionBuilder IncludeSpecial(string fieldName) /// /// Specify a special field to include in the projection. /// + /// /// The name of the field to include. /// The projection builder. public ExclusiveProjectionBuilder IncludeSpecial(Expression> fieldExpression) diff --git a/src/DataStax.AstraDB.DataApi/Core/Query/RerankSorter.cs b/src/DataStax.AstraDB.DataApi/Core/Query/RerankSorter.cs index d7204c2..83aa13e 100644 --- a/src/DataStax.AstraDB.DataApi/Core/Query/RerankSorter.cs +++ b/src/DataStax.AstraDB.DataApi/Core/Query/RerankSorter.cs @@ -36,6 +36,7 @@ internal RerankSorter(Func commandFactory, Filter filter, CommandOpt /// /// Adds a hybrid sort using a combined string to use for lexical and vectorize parameters. /// + /// /// Combined string to use for lexical and vectorize parameters. /// The document sort builder. public RerankEnumerator Sort(string combinedSearchString) diff --git a/src/DataStax.AstraDB.DataApi/Core/Query/Sort.cs b/src/DataStax.AstraDB.DataApi/Core/Query/Sort.cs index 4f156b0..c691b47 100644 --- a/src/DataStax.AstraDB.DataApi/Core/Query/Sort.cs +++ b/src/DataStax.AstraDB.DataApi/Core/Query/Sort.cs @@ -75,6 +75,11 @@ internal Sort(string sortKey, object value) internal static Sort Hybrid(string lexical, string vectorize) => new(DataApiKeywords.Hybrid, new Dictionary { { DataApiKeywords.Lexical, lexical }, { DataApiKeywords.Vectorize, vectorize } }); internal static Sort Hybrid(string lexical, float[] vector) => new(DataApiKeywords.Hybrid, new Dictionary { { DataApiKeywords.Lexical, lexical }, { DataApiKeywords.Vector, vector } }); + + internal static Sort Lexical(string value) => new(DataApiKeywords.Lexical, value); + + internal static Sort TableLexical(string columnName, string value) => new Sort(columnName, value); + } internal class Sort : Sort @@ -90,4 +95,9 @@ internal static Sort Descending(Expression> expression) { return new Sort(expression.GetMemberNameTree(), DataApiKeywords.SortDescending); } + + internal static Sort TableLexical(Expression> expression, string value) + { + return new Sort(expression.GetMemberNameTree(), value); + } } diff --git a/src/DataStax.AstraDB.DataApi/Core/Query/SortBuilder.cs b/src/DataStax.AstraDB.DataApi/Core/Query/SortBuilder.cs index 0d0b96e..a534439 100644 --- a/src/DataStax.AstraDB.DataApi/Core/Query/SortBuilder.cs +++ b/src/DataStax.AstraDB.DataApi/Core/Query/SortBuilder.cs @@ -80,7 +80,20 @@ public SortBuilder Descending(Expression> expression) return this; } - internal SortBuilder Clone() + // /// + // /// Adds a lexical sort (for documents). + // /// + // /// + // /// + // public SortBuilder Lexical(string value) + // { + // Sorts.Add(Sort.Lexical(value)); + // return this; + // } + + + + internal virtual SortBuilder Clone() { var clone = new SortBuilder(); foreach (var sort in this.Sorts) diff --git a/src/DataStax.AstraDB.DataApi/Core/Query/TableFindManyOptions.cs b/src/DataStax.AstraDB.DataApi/Core/Query/TableFindManyOptions.cs index d9ec1de..16e9aa3 100644 --- a/src/DataStax.AstraDB.DataApi/Core/Query/TableFindManyOptions.cs +++ b/src/DataStax.AstraDB.DataApi/Core/Query/TableFindManyOptions.cs @@ -22,7 +22,7 @@ namespace DataStax.AstraDB.DataApi.Core.Query; /// A set of options to be used when finding rows in a table. /// /// -public class TableFindManyOptions : TableFindOptions, IFindManyOptions> +public class TableFindManyOptions : TableFindOptions, IFindManyOptions> { /// @@ -63,9 +63,9 @@ public class TableFindManyOptions : TableFindOptions, IFindManyOptions [JsonIgnore] internal bool? IncludeSortVector { get => _includeSortVector; set => _includeSortVector = value; } - bool? IFindManyOptions>.IncludeSortVector { get => IncludeSortVector; set => IncludeSortVector = value; } + bool? IFindManyOptions>.IncludeSortVector { get => IncludeSortVector; set => IncludeSortVector = value; } - IFindManyOptions> IFindManyOptions>.Clone() + IFindManyOptions> IFindManyOptions>.Clone() { var clone = new TableFindManyOptions { diff --git a/src/DataStax.AstraDB.DataApi/Core/Query/TableFindOptions.cs b/src/DataStax.AstraDB.DataApi/Core/Query/TableFindOptions.cs index 61a9a00..64e4b41 100644 --- a/src/DataStax.AstraDB.DataApi/Core/Query/TableFindOptions.cs +++ b/src/DataStax.AstraDB.DataApi/Core/Query/TableFindOptions.cs @@ -22,11 +22,11 @@ namespace DataStax.AstraDB.DataApi.Core.Query; /// A set of options to be used when finding a row in a table. /// /// The type of the row in the table. -public class TableFindOptions : FindOptions> +public class TableFindOptions : FindOptions> { /// /// The builder used to define the sort to apply when running the query. /// [JsonIgnore] - public override SortBuilder Sort { get; set; } + public override TableSortBuilder Sort { get; set; } } diff --git a/src/DataStax.AstraDB.DataApi/Core/Query/TableSortBuilder.cs b/src/DataStax.AstraDB.DataApi/Core/Query/TableSortBuilder.cs index 60a813b..ee9c2a0 100644 --- a/src/DataStax.AstraDB.DataApi/Core/Query/TableSortBuilder.cs +++ b/src/DataStax.AstraDB.DataApi/Core/Query/TableSortBuilder.cs @@ -102,4 +102,26 @@ public TableSortBuilder Vectorize(Expression> express base.Descending(expression); return this; } + + /// + /// Adds a lexical sort. + /// + /// + /// + /// + public TableSortBuilder Lexical(Expression> column, string value) + { + Sorts.Add(Sort.TableLexical(column, value)); + return this; + } + + internal new TableSortBuilder Clone() + { + var clone = new TableSortBuilder(); + foreach (var sort in this.Sorts) + { + clone.Sorts.Add(sort.Clone()); + } + return clone; + } } diff --git a/src/DataStax.AstraDB.DataApi/Tables/TableVectorIndex.cs b/src/DataStax.AstraDB.DataApi/Core/Results/ListUserDefinedTypeNamesResult.cs similarity index 58% rename from src/DataStax.AstraDB.DataApi/Tables/TableVectorIndex.cs rename to src/DataStax.AstraDB.DataApi/Core/Results/ListUserDefinedTypeNamesResult.cs index d462159..3deffb7 100644 --- a/src/DataStax.AstraDB.DataApi/Tables/TableVectorIndex.cs +++ b/src/DataStax.AstraDB.DataApi/Core/Results/ListUserDefinedTypeNamesResult.cs @@ -14,27 +14,20 @@ * limitations under the License. */ +using DataStax.AstraDB.DataApi.Tables; +using System.Collections.Generic; using System.Text.Json.Serialization; -namespace DataStax.AstraDB.DataApi.Tables; +namespace DataStax.AstraDB.DataApi.Core.Results; - -public class TableVectorIndex +/// +/// The result object for an operation returning a list of table names. +/// +public class ListUserDefinedTypeNamesResult { - /* - "name": example_index_name", - "definition": { - "column": "example_vector_column", - "options": { - "metric": "dot_product", - "sourceModel": "ada002" - } - } - */ - [JsonPropertyName("name")] - public string IndexName { get; set; } - - [JsonPropertyName("definition")] - public TableVectorIndexDefinition Definition { get; set; } - + /// + /// The list of table names. + /// + [JsonPropertyName("types")] + public List Types { get; set; } } \ No newline at end of file diff --git a/src/DataStax.AstraDB.DataApi/Core/Results/ListUserDefinedTypesResult.cs b/src/DataStax.AstraDB.DataApi/Core/Results/ListUserDefinedTypesResult.cs new file mode 100644 index 0000000..81151a0 --- /dev/null +++ b/src/DataStax.AstraDB.DataApi/Core/Results/ListUserDefinedTypesResult.cs @@ -0,0 +1,33 @@ +/* + * Copyright DataStax, Inc. + * + * 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 + * + * 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. + */ + +using DataStax.AstraDB.DataApi.Tables; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace DataStax.AstraDB.DataApi.Core.Results; + +/// +/// The result object for an operation returning a list of table names. +/// +public class ListUserDefinedTypesResult +{ + /// + /// The list of table names. + /// + [JsonPropertyName("types")] + public List Types { get; set; } +} \ No newline at end of file diff --git a/src/DataStax.AstraDB.DataApi/Core/TextAnalyzers.cs b/src/DataStax.AstraDB.DataApi/Core/TextAnalyzers.cs new file mode 100644 index 0000000..50bb952 --- /dev/null +++ b/src/DataStax.AstraDB.DataApi/Core/TextAnalyzers.cs @@ -0,0 +1,58 @@ +/* + * Copyright DataStax, Inc. + * + * 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 + * + * 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. + */ + +using System.Text.Json.Serialization; + +namespace DataStax.AstraDB.DataApi.Core; + +/// +/// Standard text analyzers +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum TextAnalyzer +{ + /// + /// Filters StandardTokenizer output that divides text into terms on word boundaries and then uses the LowerCaseFilter. + /// + [JsonStringEnumMemberName("standard")] + Standard, + /// + /// Filters LetterTokenizer output that divides text into terms whenever it encounters a character which is not a letter and then uses the LowerCaseFilter. + /// + [JsonStringEnumMemberName("simple")] + Simple, + /// + /// Analyzer that uses WhitespaceTokenizer to divide text into terms whenever it encounters any whitespace character. + /// + [JsonStringEnumMemberName("whitespace")] + Whitespace, + /// + /// Filters LetterTokenizer output with LowerCaseFilter and removes Lucene’s default English stop words. + /// + [JsonStringEnumMemberName("stop")] + Stop, + /// + /// Normalizes input by applying LowerCaseFilter (no additional tokenization is performed). + /// + [JsonStringEnumMemberName("lowercase")] + Lowercase, + /// + /// Analyzer that uses KeywordTokenizer, which is an identity function ("noop") on input values and tokenizes the entire input as a single token. + /// + [JsonStringEnumMemberName("keyword")] + Keyword +} + diff --git a/src/DataStax.AstraDB.DataApi/Core/UserDefinedTypeField.cs b/src/DataStax.AstraDB.DataApi/Core/UserDefinedTypeField.cs new file mode 100644 index 0000000..e853183 --- /dev/null +++ b/src/DataStax.AstraDB.DataApi/Core/UserDefinedTypeField.cs @@ -0,0 +1,7 @@ +namespace DataStax.AstraDB.DataApi.Core; + +public class UserDefinedTypeField +{ + public string Name { get; set; } + public string Type { get; set; } +} \ No newline at end of file diff --git a/src/DataStax.AstraDB.DataApi/SerDes/ColumnConverter.cs b/src/DataStax.AstraDB.DataApi/SerDes/ColumnConverter.cs index edcd037..3ac0788 100644 --- a/src/DataStax.AstraDB.DataApi/SerDes/ColumnConverter.cs +++ b/src/DataStax.AstraDB.DataApi/SerDes/ColumnConverter.cs @@ -1,90 +1,176 @@ -/* - * Copyright DataStax, Inc. - * - * 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 - * - * 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. - */ - -using DataStax.AstraDB.DataApi.Tables; -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace DataStax.AstraDB.DataApi.SerDes; - -public class ColumnConverter : JsonConverter -{ - public override bool CanConvert(Type typeToConvert) => typeof(Column).IsAssignableFrom(typeToConvert); - - public override Column Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.StartObject) - { - throw new JsonException("Expected StartObject token"); - } - - using JsonDocument document = JsonDocument.ParseValue(ref reader); - JsonElement root = document.RootElement; - - if (!root.TryGetProperty("type", out JsonElement typeElement) || typeElement.ValueKind != JsonValueKind.String) - { - throw new JsonException("Missing or invalid 'type' property in Column object."); - } - - string typeValue = typeElement.GetString(); - - string jsonText = root.GetRawText(); - - if (typeValue == "vector") - { - if (root.TryGetProperty("service", out _)) - { - return JsonSerializer.Deserialize(jsonText, options) - ?? throw new JsonException("Deserialization returned null for VectorGenerationColumn"); - } - return JsonSerializer.Deserialize(jsonText, options); - } - - return typeValue switch - { - "text" => JsonSerializer.Deserialize(jsonText, options), - "ascii" => JsonSerializer.Deserialize(jsonText, options), - "varchar" => JsonSerializer.Deserialize(jsonText, options), - "inet" => JsonSerializer.Deserialize(jsonText, options), - "int" => JsonSerializer.Deserialize(jsonText, options), - "tinyint" => JsonSerializer.Deserialize(jsonText, options), - "smallint" => JsonSerializer.Deserialize(jsonText, options), - "varint" => JsonSerializer.Deserialize(jsonText, options), - "bigint" => JsonSerializer.Deserialize(jsonText, options), - "decimal" => JsonSerializer.Deserialize(jsonText, options), - "double" => JsonSerializer.Deserialize(jsonText, options), - "float" => JsonSerializer.Deserialize(jsonText, options), - "map" => JsonSerializer.Deserialize(jsonText, options), - "set" => JsonSerializer.Deserialize(jsonText, options), - "list" => JsonSerializer.Deserialize(jsonText, options), - "boolean" => JsonSerializer.Deserialize(jsonText, options), - "date" => JsonSerializer.Deserialize(jsonText, options), - "time" => JsonSerializer.Deserialize(jsonText, options), - "timestamp" => JsonSerializer.Deserialize(jsonText, options), - "vector" => JsonSerializer.Deserialize(jsonText, options), - "uuid" => JsonSerializer.Deserialize(jsonText, options), - "blob" => JsonSerializer.Deserialize(jsonText, options), - "duration" => JsonSerializer.Deserialize(jsonText, options), - _ => throw new JsonException($"Unknown Column type '{typeValue}' encountered.") - }; - } - - public override void Write(Utf8JsonWriter writer, Column value, JsonSerializerOptions options) - { - JsonSerializer.Serialize(writer, value, value.GetType(), options); - } -} +// /* +// * Copyright DataStax, Inc. +// * +// * 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 +// * +// * 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. +// */ + +// using DataStax.AstraDB.DataApi.Core; +// using DataStax.AstraDB.DataApi.Tables; +// using System; +// using System.Text.Json; +// using System.Text.Json.Serialization; + +// namespace DataStax.AstraDB.DataApi.SerDes; + +// /// +// /// JsonConverter to handle Table Columns +// /// +// public class ColumnConverter : JsonConverter +// { +// /// +// /// Check applicability of this converter +// /// +// /// +// /// +// public override bool CanConvert(Type typeToConvert) => typeof(Column).IsAssignableFrom(typeToConvert); + +// /// +// /// Handle read. +// /// +// /// +// /// +// /// +// /// +// /// +// public override Column Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) +// { +// if (reader.TokenType != JsonTokenType.StartObject) +// { +// throw new JsonException("Expected StartObject token"); +// } + +// using JsonDocument document = JsonDocument.ParseValue(ref reader); +// JsonElement root = document.RootElement; + +// if (!root.TryGetProperty("type", out JsonElement typeElement) || typeElement.ValueKind != JsonValueKind.String) +// { +// throw new JsonException("Missing or invalid 'type' property in Column object."); +// } + +// string typeValue = typeElement.GetString(); + +// // Manually construct the appropriate Column-derived object to avoid calling +// // JsonSerializer.Deserialize on types that would again use this converter. +// if (string.Equals(typeValue, "vector", StringComparison.OrdinalIgnoreCase)) +// { +// // vector types contain a "dimension" and optionally a "service" +// int dimension = 0; +// if (root.TryGetProperty("dimension", out JsonElement dimEl) && dimEl.ValueKind == JsonValueKind.Number) +// { +// dimEl.TryGetInt32(out dimension); +// } + +// if (root.TryGetProperty("service", out JsonElement serviceEl)) +// { +// var vecGen = new VectorizeColumn(); +// // dimension +// vecGen.Dimension = dimension; + +// // service options - use JsonSerializer for nested non-Column type +// if (serviceEl.ValueKind != JsonValueKind.Null) +// { +// vecGen.ServiceOptions = JsonSerializer.Deserialize(serviceEl.GetRawText(), options); +// } + +// // optional keyType/valueType +// if (root.TryGetProperty("keyType", out JsonElement kt) && kt.ValueKind == JsonValueKind.String) +// { +// vecGen.KeyType = (DataApiType)Enum.Parse(typeof(DataApiType), kt.GetString(), true); +// } +// if (root.TryGetProperty("valueType", out JsonElement vt) && vt.ValueKind == JsonValueKind.String) +// { +// vecGen.ValueType = (DataApiType)Enum.Parse(typeof(DataApiType), vt.GetString(), true); +// } + +// return vecGen; +// } + +// var vecCol = new VectorColumn(); +// vecCol.Dimension = dimension; +// if (root.TryGetProperty("keyType", out JsonElement kt2) && kt2.ValueKind == JsonValueKind.String) +// { +// vecCol.KeyType = (DataApiType)Enum.Parse(typeof(DataApiType), kt2.GetString(), true); +// } +// if (root.TryGetProperty("valueType", out JsonElement vt2) && vt2.ValueKind == JsonValueKind.String) +// { +// vecCol.ValueType = (DataApiType)Enum.Parse(typeof(DataApiType), vt2.GetString(), true); +// } + +// return vecCol; +// } + +// // Default: plain Column +// var col = new Column(); +// if (!string.IsNullOrEmpty(typeValue)) +// { +// col.Type = (DataApiType)Enum.Parse(typeof(DataApiType), typeValue, true); +// } + +// if (root.TryGetProperty("keyType", out JsonElement keyEl) && keyEl.ValueKind == JsonValueKind.String) +// { +// col.KeyType = (DataApiType)Enum.Parse(typeof(DataApiType), keyEl.GetString(), true); +// } + +// if (root.TryGetProperty("valueType", out JsonElement valEl) && valEl.ValueKind == JsonValueKind.String) +// { +// col.ValueType = (DataApiType)Enum.Parse(typeof(DataApiType), valEl.GetString(), true); +// } + +// return col; +// } + +// /// +// /// Handle write. +// /// +// /// +// /// +// /// +// public override void Write(Utf8JsonWriter writer, Column value, JsonSerializerOptions options) +// { +// if (value == null) +// { +// writer.WriteNullValue(); +// return; +// } + +// writer.WriteStartObject(); + +// if (value.Type != DataApiType.None) +// { +// writer.WriteString("type", value.Type.ToString().ToLowerInvariant()); +// } + +// if (value.KeyType != DataApiType.None) +// { +// writer.WriteString("keyType", value.KeyType.ToString().ToLowerInvariant()); +// } + +// if (value.ValueType != DataApiType.None) +// { +// writer.WriteString("valueType", value.ValueType.ToString().ToLowerInvariant()); +// } + +// if (value is VectorColumn vec) +// { +// writer.WriteNumber("dimension", vec.Dimension); + +// if (vec is VectorizeColumn vgen && vgen.ServiceOptions != null) +// { +// writer.WritePropertyName("service"); +// JsonSerializer.Serialize(writer, vgen.ServiceOptions, options); +// } +// } + +// writer.WriteEndObject(); +// } +// } diff --git a/src/DataStax.AstraDB.DataApi/SerDes/DateTimeAsDollarDateConverter.cs b/src/DataStax.AstraDB.DataApi/SerDes/DateTimeAsDollarDateConverter.cs new file mode 100644 index 0000000..4692183 --- /dev/null +++ b/src/DataStax.AstraDB.DataApi/SerDes/DateTimeAsDollarDateConverter.cs @@ -0,0 +1,122 @@ +/* + * Copyright DataStax, Inc. + * + * 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 + * + * 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. + */ + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace DataStax.AstraDB.DataApi.SerDes; + +/// +/// A custom converter to handle DataApi DateTime values +/// +/// +public class DateTimeAsDollarDateConverter : JsonConverter +{ + private static readonly DateTimeOffset UnixEpoch = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); + + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + return default; + + if (reader.TokenType == JsonTokenType.StartObject) + { + if (reader.Read() && reader.TokenType == JsonTokenType.PropertyName && reader.GetString() == "$date") + { + reader.Read(); + if (reader.TokenType != JsonTokenType.Number) + { + throw new JsonException("Expected number for Unix timestamp"); + } + + long unixTimeMilliseconds = reader.GetInt64(); + reader.Read(); + if (reader.TokenType != JsonTokenType.EndObject) + { + throw new JsonException("Expected end of object"); + } + + DateTimeOffset dto = UnixEpoch.AddMilliseconds(unixTimeMilliseconds); + + if (typeof(T) == typeof(DateTimeOffset)) + { + return (T)(object)dto; + } + else if (typeof(T) == typeof(DateTime)) + { + return (T)(object)dto.UtcDateTime; + } + else + { + throw new JsonException($"Cannot convert Unix timestamp to {typeof(T)}"); + } + } + else + { + throw new JsonException("Expected '$date' property."); + } + } + else if (reader.TokenType == JsonTokenType.Number) + { + long unixTimeMilliseconds = reader.GetInt64(); + DateTimeOffset dto = UnixEpoch.AddMilliseconds(unixTimeMilliseconds); + + if (typeof(T) == typeof(DateTimeOffset)) + { + return (T)(object)dto; + } + else if (typeof(T) == typeof(DateTime)) + { + return (T)(object)dto.UtcDateTime; + } + else + { + throw new JsonException($"Cannot convert Unix timestamp to {typeof(T)}"); + } + } + else + { + return default; + } + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + if (value == null) + { + writer.WriteNullValue(); + return; + } + + long timestampMilliseconds; + switch (value) + { + case DateTime dt: + timestampMilliseconds = (long)(dt.ToUniversalTime() - UnixEpoch).TotalMilliseconds; + break; + case DateTimeOffset dto: + timestampMilliseconds = (long)(dto - UnixEpoch).TotalMilliseconds; + break; + default: + throw new JsonException($"Unsupported type: {value.GetType()}"); + } + + writer.WriteStartObject(); + writer.WriteNumber("$date", timestampMilliseconds); + writer.WriteEndObject(); + } +} \ No newline at end of file diff --git a/src/DataStax.AstraDB.DataApi/SerDes/DateTimeConverter.cs b/src/DataStax.AstraDB.DataApi/SerDes/DateTimeConverter.cs index 2a414cf..940e4e3 100644 --- a/src/DataStax.AstraDB.DataApi/SerDes/DateTimeConverter.cs +++ b/src/DataStax.AstraDB.DataApi/SerDes/DateTimeConverter.cs @@ -14,87 +14,76 @@ * limitations under the License. */ + using System; using System.Text.Json; using System.Text.Json.Serialization; -namespace DataStax.AstraDB.DataApi.SerDes; - /// -/// A custom converter to handle DataApi DateTime values +/// Handle serialization of DateTime when Kind is Unspecified /// -/// -public class DateTimeConverter : JsonConverter +public class DateTimeConverter : JsonConverter { - private static readonly DateTimeOffset UnixEpoch = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); + /// + /// Default read handling + /// + /// + /// + /// + /// + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.GetDateTime(); + } - public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + /// + /// Set Kind to Local when Unspecified + /// + /// + /// + /// + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) { - if (reader.TokenType == JsonTokenType.Null) - return default; + DateTime dateTimeToWrite = value; - if (reader.TokenType == JsonTokenType.StartObject) + if (value.Kind == DateTimeKind.Unspecified) { - if (reader.Read() && reader.TokenType == JsonTokenType.PropertyName && reader.GetString() == "$date") - { - reader.Read(); - if (reader.TokenType != JsonTokenType.Number) - { - throw new JsonException("Expected number for Unix timestamp"); - } - - long unixTimeMilliseconds = reader.GetInt64(); - reader.Read(); - if (reader.TokenType != JsonTokenType.EndObject) - { - throw new JsonException("Expected end of object"); - } - - DateTimeOffset dto = UnixEpoch.AddMilliseconds(unixTimeMilliseconds); - - if (typeof(T) == typeof(DateTimeOffset)) - { - return (T)(object)dto; - } - else if (typeof(T) == typeof(DateTime)) - { - return (T)(object)dto.UtcDateTime; - } - else - { - throw new JsonException($"Cannot convert Unix timestamp to {typeof(T)}"); - } - } - else - { - throw new JsonException("Expected '$date' property."); - } + dateTimeToWrite = DateTime.SpecifyKind(value, DateTimeKind.Local); } - else if (reader.TokenType == JsonTokenType.Number) - { - long unixTimeMilliseconds = reader.GetInt64(); - DateTimeOffset dto = UnixEpoch.AddMilliseconds(unixTimeMilliseconds); - if (typeof(T) == typeof(DateTimeOffset)) - { - return (T)(object)dto; - } - else if (typeof(T) == typeof(DateTime)) - { - return (T)(object)dto.UtcDateTime; - } - else - { - throw new JsonException($"Cannot convert Unix timestamp to {typeof(T)}"); - } - } - else + writer.WriteStringValue(dateTimeToWrite); + } +} + +/// +/// Handle serialization of DateTime? when Kind is Unspecified +/// +public class DateTimeNullableConverter : JsonConverter +{ + /// + /// Use default deserialization + /// + /// + /// + /// + /// + public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) { - return default; + return null; } + + return reader.GetDateTime(); } - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + /// + /// If Kind is Unspecified, use Local + /// + /// + /// + /// + public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options) { if (value == null) { @@ -102,21 +91,13 @@ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions return; } - long timestampMilliseconds; - switch (value) + DateTime dateTimeToWrite = value.Value; + + if (value.Value.Kind == DateTimeKind.Unspecified) { - case DateTime dt: - timestampMilliseconds = (long)(dt.ToUniversalTime() - UnixEpoch).TotalMilliseconds; - break; - case DateTimeOffset dto: - timestampMilliseconds = (long)(dto - UnixEpoch).TotalMilliseconds; - break; - default: - throw new JsonException($"Unsupported type: {value.GetType()}"); + dateTimeToWrite = DateTime.SpecifyKind(value.Value, DateTimeKind.Local); } - writer.WriteStartObject(); - writer.WriteNumber("$date", timestampMilliseconds); - writer.WriteEndObject(); + writer.WriteStringValue(dateTimeToWrite); } } \ No newline at end of file diff --git a/src/DataStax.AstraDB.DataApi/SerDes/IdListConverter.cs b/src/DataStax.AstraDB.DataApi/SerDes/IdListConverter.cs index c54aa6d..a3d5d97 100644 --- a/src/DataStax.AstraDB.DataApi/SerDes/IdListConverter.cs +++ b/src/DataStax.AstraDB.DataApi/SerDes/IdListConverter.cs @@ -32,7 +32,7 @@ public class IdListConverter : JsonConverter> { private static readonly GuidConverter _guidConverter = new(); private static readonly ObjectIdConverter _objectIdConverter = new(); - private static readonly DateTimeConverter _dateTimeConverter = new(); + private static readonly DateTimeAsDollarDateConverter _dateTimeConverter = new(); public override List Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/src/DataStax.AstraDB.DataApi/SerDes/RowConverter.cs b/src/DataStax.AstraDB.DataApi/SerDes/RowConverter.cs index 4847cf1..b2fb952 100644 --- a/src/DataStax.AstraDB.DataApi/SerDes/RowConverter.cs +++ b/src/DataStax.AstraDB.DataApi/SerDes/RowConverter.cs @@ -139,6 +139,6 @@ private static string GetPropertyName(PropertyInfo property, bool forDeserializa } var nameAttribute = property.GetCustomAttribute(); - return nameAttribute == null ? property.Name : nameAttribute.ColumnName; + return nameAttribute == null ? property.Name : nameAttribute.Name; } } \ No newline at end of file diff --git a/src/DataStax.AstraDB.DataApi/Tables/AlterUserDefinedTypeDefinition.cs b/src/DataStax.AstraDB.DataApi/Tables/AlterUserDefinedTypeDefinition.cs new file mode 100644 index 0000000..e49706c --- /dev/null +++ b/src/DataStax.AstraDB.DataApi/Tables/AlterUserDefinedTypeDefinition.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +public class AlterUserDefinedTypeDefinition +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the User Defined Type to alter. + public AlterUserDefinedTypeDefinition(string name) + { + Name = name; + } + + /// + /// The name of the User Defined Type to alter. + /// + [JsonPropertyName("name")] + public string Name { get; set; } + + /// + /// The fields to rename. + /// + [JsonPropertyName("rename")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public RenameFieldsDefinition Rename { get; set; } + + /// + /// The fields to add. + /// + [JsonPropertyName("add")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public AddFieldsDefinition Add { get; set; } + + /// + /// Adds a field to the User Defined Type. + /// + /// The field to add. + /// The current instance of . + public AlterUserDefinedTypeDefinition AddField(string field, DataApiType fieldType) + { + Add ??= new AddFieldsDefinition(); + Add.Fields ??= new Dictionary(); + Add.Fields.Add(field, fieldType.Key); + return this; + } + + /// + /// Renames a field in the User Defined Type. + /// + /// The name of the field to rename. + /// The new name of the field. + /// The current instance of . + public AlterUserDefinedTypeDefinition RenameField(string fieldName, string newFieldName) + { + Rename ??= new RenameFieldsDefinition(); + Rename.Fields ??= new Dictionary(); + Rename.Fields.Add(fieldName, newFieldName); + return this; + } +} + +public class RenameFieldsDefinition +{ + [JsonPropertyName("fields")] + public Dictionary Fields { get; set; } +} + +public class AddFieldsDefinition +{ + [JsonPropertyName("fields")] + public Dictionary Fields { get; set; } +} \ No newline at end of file diff --git a/src/DataStax.AstraDB.DataApi/Tables/Column.cs b/src/DataStax.AstraDB.DataApi/Tables/Column.cs index ca1829d..7acbac1 100644 --- a/src/DataStax.AstraDB.DataApi/Tables/Column.cs +++ b/src/DataStax.AstraDB.DataApi/Tables/Column.cs @@ -1,334 +1,178 @@ -/* - * Copyright DataStax, Inc. - * - * 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 - * - * 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. - */ - -using DataStax.AstraDB.DataApi.Core; -using DataStax.AstraDB.DataApi.SerDes; -using System.Text.Json.Serialization; - -namespace DataStax.AstraDB.DataApi.Tables; - - -/// -/// Represents a column in a table -/// -[JsonConverter(typeof(ColumnConverter))] -public abstract class Column -{ - /// - /// The type of the column - /// - [JsonIgnore] - public abstract string Type { get; } -} - -/// -/// A column that holds text values -/// -public class TextColumn : Column -{ - /// - /// Defines the column as type text - /// - [JsonPropertyName("type")] - public override string Type => "text"; -} - -/// -/// A column that holds UUID values -/// -public class GuidColumn : Column -{ - /// - /// Defines the column as type uuid - /// - [JsonPropertyName("type")] - public override string Type => "uuid"; -} - -/// -/// A column that holds integer values -/// -public class IntColumn : Column -{ - /// - /// Defines the column as type int - /// - [JsonPropertyName("type")] - public override string Type => "int"; -} - -/// -/// A column that holds long integer values -/// -public class LongColumn : Column -{ - /// - /// Defines the column as type bigint - /// - [JsonPropertyName("type")] - public override string Type => "bigint"; -} - -/// -/// A column that holds decimal values -/// -public class DecimalColumn : Column -{ - /// - /// Defines the column as type decimal - /// - [JsonPropertyName("type")] - public override string Type => "decimal"; -} - -/// -/// A column that holds float values -/// -public class FloatColumn : Column -{ - /// - /// Defines the column as type float - /// - [JsonPropertyName("type")] - public override string Type => "float"; -} - -/// -/// A column that holds double values -/// -public class DoubleColumn : Column -{ - /// - /// Defines the column as type double - /// - [JsonPropertyName("type")] - public override string Type => "double"; -} - -/// -/// A column that holds boolean values -/// -public class BooleanColumn : Column -{ - /// - /// Defines the column as type boolean - /// - [JsonPropertyName("type")] - public override string Type => "boolean"; -} - -/// -/// A column that holds date/time values -/// -public class DateTimeColumn : Column -{ - /// - /// Defines the column as type timestamp - /// - [JsonPropertyName("type")] - public override string Type => "timestamp"; -} - -/// -/// A column that holds binary values -/// -public class BlobColumn : Column -{ - /// - /// Defines the column as type blob - /// - [JsonPropertyName("type")] - public override string Type => "blob"; -} - -/// -/// A column that holds IP address values -/// -public class IPAddressColumn : Column -{ - /// - /// Defines the column as type inet - /// - [JsonPropertyName("type")] - public override string Type => "inet"; -} - -/// -/// A column that holds duration values -/// -public class DurationColumn : Column -{ - /// - /// Defines the column as type duration - /// - [JsonPropertyName("type")] - public override string Type => "duration"; -} - -/// -/// A column that holds dictionary/map values -/// -public class DictionaryColumn : Column -{ - /// - /// Defines the column as type map - /// - [JsonPropertyName("type")] - public override string Type => "map"; - - /// - /// The type of the keys in the dictionary - /// - [JsonPropertyName("keyType")] - public string KeyType { get; set; } - - /// - /// The type of the values in the dictionary - /// - [JsonPropertyName("valueType")] - public string ValueType { get; set; } - - internal DictionaryColumn(string keyType, string valueType) - { - KeyType = keyType; - ValueType = valueType; - } - - /// - /// Creates a new instance of the class. - /// - public DictionaryColumn() { } -} - -/// -/// A column that holds list/array values -/// -public class ListColumn : Column -{ - /// - /// Defines the column as type list - /// - [JsonPropertyName("type")] - public override string Type => "list"; - - /// - /// The type of the values in the list - /// - [JsonPropertyName("valueType")] - public string ValueType { get; set; } - - internal ListColumn(string valueType) - { - ValueType = valueType; - } - - /// - /// Creates a new instance of the class. - /// - public ListColumn() { } -} - -/// -/// A column that holds set values -/// -public class SetColumn : Column -{ - /// - /// Defines the column as type set - /// - [JsonPropertyName("type")] - public override string Type => "set"; - - /// - /// The type of the values in the set - /// - [JsonPropertyName("valueType")] - public string ValueType { get; set; } - - internal SetColumn(string valueType) - { - ValueType = valueType; - } - - /// - /// Creates a new instance of the class. - /// - public SetColumn() { } -} - -/// -/// A column that holds vector values -/// -public class VectorColumn : Column -{ - /// - /// Defines the column as type vector - /// - [JsonPropertyName("type")] - public override string Type => "vector"; - - /// - /// The dimension of the vector - /// - [JsonPropertyName("dimension")] - public int Dimension { get; set; } - - internal VectorColumn(int dimension) - { - Dimension = dimension; - } - - /// - /// Creates a new instance of the class. - /// - public VectorColumn() { } -} - -/// -/// A column that holds vector values that are generated by a vectorization service -/// -public class VectorizeColumn : VectorColumn -{ - /// - /// The vectorization service options - /// - [JsonPropertyName("service")] - public VectorServiceOptions ServiceOptions { get; set; } - - /// - /// Creates a new instance of the class with the specified dimension and service options. - /// - /// - /// - public VectorizeColumn(int dimension, VectorServiceOptions serviceOptions) : base(dimension) - { - ServiceOptions = serviceOptions; - } - - /// - /// Creates a new instance of the class with the specified service options. - /// - /// - public VectorizeColumn(VectorServiceOptions serviceOptions) - { - ServiceOptions = serviceOptions; - } - - /// - /// Creates a new instance of the class. - /// - public VectorizeColumn() { } -} \ No newline at end of file +// /* +// * Copyright DataStax, Inc. +// * +// * 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 +// * +// * 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. +// */ + +// using DataStax.AstraDB.DataApi.Core; +// using DataStax.AstraDB.DataApi.SerDes; +// using System; +// using System.Text.Json.Serialization; + +// namespace DataStax.AstraDB.DataApi.Tables; + + +// /// +// /// Represents a column in a table +// /// +// [JsonConverter(typeof(ColumnConverter))] +// public class Column +// { +// [JsonPropertyName("type")] +// public string TypeKey { get; set; } + +// // [JsonPropertyName("type")] +// // [JsonInclude] +// // internal string TypeString +// // { +// // get { return Type == DataApiType.None ? null : Type.ToString().ToLowerInvariant(); } +// // set { Type = value == null ? DataApiType.None : (DataApiType)Enum.Parse(typeof(DataApiType), value, true); } +// // } + +// // [JsonPropertyName("keyType")] +// // [JsonInclude] +// // internal string KeyTypeString +// // { +// // get { return KeyType == DataApiType.None ? null : KeyType.ToString().ToLowerInvariant(); } +// // set { KeyType = value == null ? DataApiType.None : (DataApiType)Enum.Parse(typeof(DataApiType), value, true); } +// // } + +// // [JsonPropertyName("valueType")] +// // [JsonInclude] +// // internal string ValueTypeString +// // { +// // get { return ValueType == DataApiType.None ? null : ValueType.ToString().ToLowerInvariant(); } +// // set { ValueType = value == null ? DataApiType.None : (DataApiType)Enum.Parse(typeof(DataApiType), value, true); } +// // } + +// // /// +// // /// The type of the column +// // /// +// // //[JsonIgnore] +// // public virtual DataApiType Type { get; set; } + +// // /// +// // /// The type of the keys in the dictionary +// // /// +// // //[JsonIgnore] +// // public DataApiType KeyType { get; set; } + +// // /// +// // /// The type of the values in the dictionary +// // /// +// // //[JsonIgnore] +// // public DataApiType ValueType { get; set; } +// } + + +// /// +// /// A column that holds vector values +// /// +// public class VectorColumn : Column +// { +// // /// +// // /// Defines the column as type vector +// // /// +// // public override DataApiType Type => DataApiType.Vector; + +// /// +// /// The dimension of the vector +// /// +// [JsonPropertyName("dimension")] +// public int Dimension { get; set; } + +// // internal VectorColumn(int dimension) +// // { +// // Dimension = dimension; +// // } + +// // /// +// // /// Creates a new instance of the class. +// // /// +// // public VectorColumn() { } +// } + +// /// +// /// A column that holds vector values that are generated by a vectorization service +// /// +// public class VectorizeColumn : VectorColumn +// { +// /// +// /// The vectorization service options +// /// +// [JsonPropertyName("service")] +// public VectorServiceOptions ServiceOptions { get; set; } + +// // /// +// // /// Creates a new instance of the class with the specified dimension and service options. +// // /// +// // /// +// // /// +// // public VectorizeColumn(int dimension, VectorServiceOptions serviceOptions) // : base(dimension) +// // { +// // Dimension = d +// // ServiceOptions = serviceOptions; +// // } + +// // /// +// // /// Creates a new instance of the class with the specified service options. +// // /// +// // /// +// // public VectorizeColumn(VectorServiceOptions serviceOptions) +// // { +// // ServiceOptions = serviceOptions; +// // } + +// // /// +// // /// Creates a new instance of the class. +// // /// +// // public VectorizeColumn() { } +// } + +// /// +// /// A column with a User Defined Type +// /// +// public class UserDefinedTypeColumn : Column +// { +// [JsonPropertyName("udtName")] +// public string UserDefinedTypeName { get; set; } + +// // /// +// // /// Defines the column as type vector +// // /// +// // public override DataApiType Type => DataApiType.UserDefined; + +// // /// +// // /// The dimension of the vector +// // /// +// // [JsonPropertyName("dimension")] +// // public int Dimension { get; set; } + +// // internal VectorColumn(int dimension) +// // { +// // Dimension = dimension; +// // } + +// // /// +// // /// Creates a new instance of the class. +// // /// +// // public VectorColumn() { } +// } + +// public class ListColumn : Column +// { +// [JsonPropertyName("udtName")] +// public string UserDefinedTypeName { get; set; } + + +// } \ No newline at end of file diff --git a/src/DataStax.AstraDB.DataApi/Tables/ColumnNameAttribute.cs b/src/DataStax.AstraDB.DataApi/Tables/ColumnNameAttribute.cs index 19f3954..1429503 100644 --- a/src/DataStax.AstraDB.DataApi/Tables/ColumnNameAttribute.cs +++ b/src/DataStax.AstraDB.DataApi/Tables/ColumnNameAttribute.cs @@ -21,10 +21,10 @@ namespace DataStax.AstraDB.DataApi.Tables; [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] public class ColumnNameAttribute : Attribute { - public string ColumnName { get; set; } + public string Name { get; set; } public ColumnNameAttribute(string columnName) { - ColumnName = columnName; + Name = columnName; } } \ No newline at end of file diff --git a/src/DataStax.AstraDB.DataApi/Tables/ColumnTypes.cs b/src/DataStax.AstraDB.DataApi/Tables/ColumnTypes.cs deleted file mode 100644 index 5d15db8..0000000 --- a/src/DataStax.AstraDB.DataApi/Tables/ColumnTypes.cs +++ /dev/null @@ -1,4 +0,0 @@ -public enum ColumnTypes -{ - Text -} \ No newline at end of file diff --git a/src/DataStax.AstraDB.DataApi/Tables/DropUserDefinedTypeRequest.cs b/src/DataStax.AstraDB.DataApi/Tables/DropUserDefinedTypeRequest.cs new file mode 100644 index 0000000..7af4869 --- /dev/null +++ b/src/DataStax.AstraDB.DataApi/Tables/DropUserDefinedTypeRequest.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +internal class DropUserDefinedTypeRequest +{ + [JsonInclude] + [JsonPropertyName("name")] + internal string Name { get; set; } + + [JsonInclude] + [JsonPropertyName("options")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + internal Dictionary Options { get; set; } + + internal void SetSkipIfExists(bool skipIfExists) + { + var optionsKey = "ifNotExists"; + if (!skipIfExists) + { + if (Options != null) + { + Options.Remove(optionsKey); + } + } + else + { + Options ??= new Dictionary(); + Options[optionsKey] = skipIfExists; + } + } +} \ No newline at end of file diff --git a/src/DataStax.AstraDB.DataApi/Tables/Table.cs b/src/DataStax.AstraDB.DataApi/Tables/Table.cs index b0c1416..0944aad 100644 --- a/src/DataStax.AstraDB.DataApi/Tables/Table.cs +++ b/src/DataStax.AstraDB.DataApi/Tables/Table.cs @@ -23,6 +23,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; @@ -33,7 +34,7 @@ namespace DataStax.AstraDB.DataApi.Tables; /// This is the main entry point for interacting with a table in the Astra DB Data API. /// /// The type to use for rows in the table (when not specified, defaults to -public class Table : IQueryRunner> where T : class +public class Table : IQueryRunner> where T : class { private readonly string _tableName; private readonly Database _database; @@ -150,97 +151,364 @@ private async Task> ListIndexNamesAsync(CommandOptions commandOptio } /// - /// Synchronous version of + /// Synchronous version of /// - /// - public void CreateIndex(TableIndex index) + /// + public void CreateIndex(string indexName, Expression> column) { - CreateIndex(index, null); + CreateIndex(indexName, column.GetMemberNameTree(), null, null); } /// - /// Synchronous version of + /// Synchronous version of /// - /// - public void CreateIndex(TableIndex index, CreateIndexCommandOptions commandOptions) + /// + public void CreateIndex(string indexName, string columnName) { - CreateIndexAsync(index, commandOptions, true).ResultSync(); + CreateIndex(indexName, columnName, null, null); } /// - /// Creates an index on the table. + /// Synchronous version of /// - /// The index specifications - public Task CreateIndexAsync(TableIndex index) + /// + public void CreateIndex(Expression> column) { - return CreateIndexAsync(index, null, false); + CreateIndex(null, column.GetMemberNameTree(), null, null); } - /// - /// - public Task CreateIndexAsync(TableIndex index, CreateIndexCommandOptions commandOptions) + /// + /// Synchronous version of + /// + /// + public void CreateIndex(string columnName) { - return CreateIndexAsync(index, commandOptions, false); + CreateIndex(null, columnName, null, null); } - private async Task CreateIndexAsync(TableIndex index, CreateIndexCommandOptions commandOptions, bool runSynchronously) + /// + /// Synchronous version of + /// + /// + public void CreateIndex(Expression> column, CreateIndexCommandOptions commandOptions) { - var indexResponse = await ListIndexMetadataAsync(commandOptions, runSynchronously); - var exists = indexResponse?.Indexes?.Any(i => i.Name == index.IndexName) == true; + CreateIndex(null, column.GetMemberNameTree(), null, commandOptions); + } - if (exists) - { - if (commandOptions != null && commandOptions.SkipIfExists) - { - return; - } - throw new InvalidOperationException($"Index '{index.IndexName}' already exists on table '{this._tableName}'."); - } + /// + /// Synchronous version of + /// + /// + public void CreateIndex(string columnName, CreateIndexCommandOptions commandOptions) + { + CreateIndex(null, columnName, null, commandOptions); + } - var command = CreateCommand("createIndex").WithPayload(index).AddCommandOptions(commandOptions); - await command.RunAsyncReturnStatus>(runSynchronously).ConfigureAwait(false); + /// + /// Synchronous version of + /// + /// + public void CreateIndex(string columnName, TableIndexDefinition indexDefinition) + { + CreateIndex(null, columnName, indexDefinition, null); + } + + /// + /// Synchronous version of + /// + /// + public void CreateIndex(Expression> column, TableIndexDefinition indexDefinition, CreateIndexCommandOptions commandOptions) + { + CreateIndex(null, column.GetMemberNameTree(), indexDefinition, commandOptions); + } + + /// + /// Synchronous version of + /// + /// + public void CreateIndex(string columnName, TableIndexDefinition indexDefinition, CreateIndexCommandOptions commandOptions) + { + CreateIndex(null, columnName, indexDefinition, commandOptions); + } + + /// + /// Synchronous version of + /// + /// + public void CreateIndex(string indexName, Expression> column, CreateIndexCommandOptions commandOptions) + { + CreateIndex(indexName, column.GetMemberNameTree(), null, commandOptions); + } + + /// + /// Synchronous version of + /// + /// + public void CreateIndex(string indexName, string columnName, CreateIndexCommandOptions commandOptions) + { + CreateIndex(indexName, columnName, null, commandOptions); + } + + /// + /// Synchronous version of + /// + /// + public void CreateIndex(string indexName, Expression> column, TableIndexDefinition indexDefinition) + { + CreateIndex(indexName, column.GetMemberNameTree(), indexDefinition, null); + } + + /// + /// Synchronous version of + /// + /// + public void CreateIndex(string indexName, string columnName, TableIndexDefinition indexDefinition) + { + CreateIndex(indexName, columnName, indexDefinition, null); + } + + /// + /// Synchronous version of + /// + /// + public void CreateIndex(Expression> column, TableIndexDefinition indexDefinition) + { + CreateIndex(null, column.GetMemberNameTree(), indexDefinition, null); } /// - /// Synchronous version of + /// Synchronous version of /// - /// - public void CreateVectorIndex(TableVectorIndex index) + /// + public void CreateIndex(string indexName, Expression> column, TableIndexDefinition indexDefinition, CreateIndexCommandOptions commandOptions) { - CreateVectorIndex(index, null); + CreateIndex(indexName, column.GetMemberNameTree(), indexDefinition, commandOptions); } /// - /// Synchronous version of + /// Synchronous version of /// - /// - public void CreateVectorIndex(TableVectorIndex index, CreateIndexCommandOptions commandOptions) + /// + public void CreateIndex(string indexName, string columnName, TableIndexDefinition indexDefinition, CreateIndexCommandOptions commandOptions) + { + CreateIndexAsync(indexName, columnName, indexDefinition, commandOptions, false).ResultSync(); + } + + /// + /// Create a text index on the specified column, using default options + /// + /// The type of the column to index + /// The column to index + /// + /// Index name will be generated as "{columnName}_idx". Use an overload that accepts an index name if you want to specify a custom name. + /// + /// + public Task CreateIndexAsync(Expression> column) { - CreateVectorIndexAsync(index, commandOptions, true).ResultSync(); + return CreateIndexAsync(null, column.GetMemberNameTree(), null, null); } /// - /// Creates a vector index on the table. + /// Create a text index on the specified column, using default options /// - /// - public Task CreateVectorIndexAsync(TableVectorIndex index) + /// The name of the column to index + /// + /// This will create a standard index on the column. If you want to create a Text Index (Lexical) or Vector Index, use an overload + /// that takes a parameter, and use the to create the appropriate definition. + /// + /// + /// Index name will be generated as "{columnName}_idx". Use an overload that accepts an index name if you want to specify a custom name. + /// + /// + public Task CreateIndexAsync(string columnName) + { + return CreateIndexAsync(null, columnName, null, null); + } + + /// + /// Create a text index on the specified column, using default options + /// + /// The type of the column to index + /// The index name + /// The column to index + /// + public Task CreateIndexAsync(string indexName, Expression> column) + { + return CreateIndexAsync(indexName, column.GetMemberNameTree(), null, null); + } + + /// + /// Create a text index on the specified column, using default options + /// + /// The index name + /// The name of the column to index + /// + /// This will create a standard index on the column. If you want to create a Text Index (Lexical) or Vector Index, use an overload + /// that takes a parameter, and use the to create the appropriate definition. + /// + /// + public Task CreateIndexAsync(string indexName, string columnName) + { + return CreateIndexAsync(indexName, columnName, null, null); + } + + /// + /// The column to index + /// + /// + /// Index name will be generated as "{columnName}_idx". Use an overload that accepts an index name if you want to specify a custom name. + /// + public Task CreateIndexAsync(Expression> column, CreateIndexCommandOptions commandOptions) + { + return CreateIndexAsync(null, column.GetMemberNameTree(), null, commandOptions); + } + + /// + /// The index name + /// The column to index + /// + public Task CreateIndexAsync(string indexName, Expression> column, CreateIndexCommandOptions commandOptions) + { + return CreateIndexAsync(indexName, column.GetMemberNameTree(), null, commandOptions); + } + + /// + /// The name of the column to index + /// + /// + /// Index name will be generated as "{columnName}_idx". Use an overload that accepts an index name if you want to specify a custom name. + /// + public Task CreateIndexAsync(string columnName, CreateIndexCommandOptions commandOptions) + { + return CreateIndexAsync(null, columnName, null, commandOptions); + } + + /// + /// The index name + /// The name of the column to index + /// + public Task CreateIndexAsync(string indexName, string columnName, CreateIndexCommandOptions commandOptions) + { + return CreateIndexAsync(indexName, columnName, null, commandOptions); + } + + /// + /// The index name + /// The column to index + /// Use to create the appropriate index definition. + public Task CreateIndexAsync(string indexName, Expression> column, TableIndexDefinition indexDefinition) + { + return CreateIndexAsync(indexName, column.GetMemberNameTree(), indexDefinition, null); + } + + /// + /// The name of the column to index + /// Use to create the appropriate index definition. + /// + /// Index name will be generated as "{columnName}_idx". Use an overload that accepts an index name if you want to specify a custom name. + /// + public Task CreateIndexAsync(string columnName, TableIndexDefinition indexDefinition) + { + return CreateIndexAsync(null, columnName, indexDefinition, null); + } + + /// + /// The column to index + /// Use to create the appropriate index definition. + /// + /// Index name will be generated as "{columnName}_idx". Use an overload that accepts an index name if you want to specify a custom name. + /// + public Task CreateIndexAsync(Expression> column, TableIndexDefinition indexDefinition) + { + return CreateIndexAsync(null, column.GetMemberNameTree(), indexDefinition, null); + } + + /// + /// The index name + /// The name of the column to index + /// Use to create the appropriate index definition. + public Task CreateIndexAsync(string indexName, string columnName, TableIndexDefinition indexDefinition) + { + return CreateIndexAsync(indexName, columnName, indexDefinition, null); + } + + /// + /// The column to index + /// Use to create the appropriate index definition. + /// + /// + /// Index name will be generated as "{columnName}_idx". Use an overload that accepts an index name if you want to specify a custom name. + /// + public Task CreateIndexAsync(Expression> column, TableIndexDefinition indexDefinition, CreateIndexCommandOptions commandOptions) + { + return CreateIndexAsync(null, column.GetMemberNameTree(), indexDefinition, commandOptions); + } + + /// + /// The index name + /// The column to index + /// Use to create the appropriate index definition. + /// + public Task CreateIndexAsync(string indexName, Expression> column, TableIndexDefinition indexDefinition, CreateIndexCommandOptions commandOptions) { - return CreateVectorIndexAsync(index, null, false); + return CreateIndexAsync(indexName, column.GetMemberNameTree(), indexDefinition, commandOptions); } - /// + /// + /// The name of the column to index + /// Use to create the appropriate index definition. /// - public Task CreateVectorIndexAsync(TableVectorIndex index, CreateIndexCommandOptions commandOptions) + /// + /// Index name will be generated as "{columnName}_idx". Use an overload that accepts an index name if you want to specify a custom name. + /// + public Task CreateIndexAsync(string columnName, TableIndexDefinition indexDefinition, CreateIndexCommandOptions commandOptions) { - return CreateVectorIndexAsync(index, commandOptions, false); + return CreateIndexAsync(null, columnName, indexDefinition, commandOptions, true); } - private async Task CreateVectorIndexAsync(TableVectorIndex index, CreateIndexCommandOptions commandOptions, bool runSynchronously) + /// + /// The index name + /// The name of the column to index + /// Use to create the appropriate index definition. + /// + public Task CreateIndexAsync(string indexName, string columnName, TableIndexDefinition indexDefinition, CreateIndexCommandOptions commandOptions) { - var command = CreateCommand("createVectorIndex").WithPayload(index).AddCommandOptions(commandOptions); + return CreateIndexAsync(indexName, columnName, indexDefinition, commandOptions, true); + } + + private async Task CreateIndexAsync(string indexName, string columnName, TableIndexDefinition indexDefinition, CreateIndexCommandOptions commandOptions, bool runSynchronously) + { + if (indexName == null) + { + indexName = $"{columnName}_idx"; + } + var indexResponse = await ListIndexMetadataAsync(commandOptions, runSynchronously); + var exists = indexResponse?.Indexes?.Any(i => i.Name == indexName) == true; + + if (exists) + { + if (commandOptions != null && commandOptions.SkipIfExists) + { + return; + } + throw new InvalidOperationException($"Index '{indexName}' already exists on table '{this._tableName}'."); + } + if (indexDefinition == null) + { + indexDefinition = new TableIndexDefinition(); + } + indexDefinition.ColumnName = columnName; + var index = new TableIndex + { + IndexName = indexName, + Definition = indexDefinition + }; + var command = CreateCommand(indexDefinition.IndexCreationCommandName).WithPayload(index).AddCommandOptions(commandOptions); await command.RunAsyncReturnStatus>(runSynchronously).ConfigureAwait(false); } + // + /// /// This is a synchronous version of /// @@ -285,6 +553,7 @@ public Task InsertManyAsync(IEnumerable rows) } /// + /// /// public Task InsertManyAsync(IEnumerable rows, CommandOptions commandOptions) { @@ -292,6 +561,7 @@ public Task InsertManyAsync(IEnumerable rows, CommandO } /// + /// /// public Task InsertManyAsync(IEnumerable rows, InsertManyOptions insertOptions) { @@ -299,7 +569,9 @@ public Task InsertManyAsync(IEnumerable rows, InsertMa } /// + /// /// + /// public Task InsertManyAsync(IEnumerable rows, InsertManyOptions insertOptions, CommandOptions commandOptions) { return InsertManyAsync(rows, insertOptions, commandOptions, false); @@ -411,6 +683,7 @@ public Task InsertOneAsync(T row) } /// + /// /// public Task InsertOneAsync(T row, CommandOptions commandOptions) { @@ -432,11 +705,11 @@ private async Task InsertOneAsync(T row, CommandOptions co /// /// Find rows in the table. /// - /// The Find() methods return a object that can be used to further structure the query + /// The Find() methods return a object that can be used to further structure the query /// by adding Sort, Projection, Skip, Limit, etc. to affect the final results. /// - /// The object can be directly enumerated both synchronously and asynchronously. - /// Secondarily, the results can be paged through manually by using the results of . + /// The object can be directly enumerated both synchronously and asynchronously. + /// Secondarily, the results can be paged through manually by using the results of . /// /// /// @@ -465,7 +738,7 @@ private async Task InsertOneAsync(T row, CommandOptions co /// however settings are ignored due to the nature of Enueration. /// If you need to enforce a timeout for the entire operation, you can pass a to GetAsyncEnumerator. /// - public FindEnumerator> Find() + public FindEnumerator> Find() { return Find(null, null); } @@ -484,14 +757,15 @@ public FindEnumerator> Find() /// } /// /// - public FindEnumerator> Find(Filter filter) + public FindEnumerator> Find(Filter filter) { return Find(filter, null); } /// + /// /// - public FindEnumerator> Find(Filter filter, CommandOptions commandOptions) + public FindEnumerator> Find(Filter filter, CommandOptions commandOptions) { return Find(filter, commandOptions); } @@ -503,16 +777,16 @@ public FindEnumerator> Find(Filter filter, CommandOption /// This overload of Find() allows you to specify a different result class type /// which the resultant rows will be deserialized into. This is generally used along with .Project() to limit the fields returned /// - public FindEnumerator> Find(Filter filter, CommandOptions commandOptions) where TResult : class + public FindEnumerator> Find(Filter filter, CommandOptions commandOptions) where TResult : class { var findOptions = new TableFindManyOptions { Filter = filter }; - return new FindEnumerator>(this, findOptions, commandOptions); + return new FindEnumerator>(this, findOptions, commandOptions); } - internal async Task, FindStatusResult>> RunFindManyAsync(Filter filter, IFindManyOptions> findOptions, CommandOptions commandOptions, bool runSynchronously) + internal async Task, FindStatusResult>> RunFindManyAsync(Filter filter, IFindManyOptions> findOptions, CommandOptions commandOptions, bool runSynchronously) where TResult : class { findOptions.Filter = filter; @@ -619,6 +893,7 @@ public Task FindOneAsync(Filter filter) } /// + /// /// public Task FindOneAsync(Filter filter, CommandOptions commandOptions) { @@ -636,6 +911,7 @@ public Task FindOneAsync(TableFindOptions findOptions) } /// + /// /// Specify Sort options for the find operation. public Task FindOneAsync(Filter filter, TableFindOptions findOptions) { @@ -643,6 +919,8 @@ public Task FindOneAsync(Filter filter, TableFindOptions findOptions) } /// + /// + /// /// public Task FindOneAsync(Filter filter, TableFindOptions findOptions, CommandOptions commandOptions) { @@ -669,6 +947,7 @@ public Task FindOneAsync(Filter filter) where TResult : cla } /// + /// /// public Task FindOneAsync(Filter filter, CommandOptions commandOptions) where TResult : class { @@ -676,6 +955,7 @@ public Task FindOneAsync(Filter filter, CommandOptions comm } /// + /// /// Specify Sort options for the find operation. public Task FindOneAsync(Filter filter, TableFindOptions findOptions) where TResult : class { @@ -683,6 +963,8 @@ public Task FindOneAsync(Filter filter, TableFindOptions } /// + /// + /// /// public Task FindOneAsync(Filter filter, TableFindOptions findOptions, CommandOptions commandOptions) where TResult : class { @@ -769,6 +1051,8 @@ public Task UpdateOneAsync(Filter filter, UpdateBuilder update) } /// + /// + /// /// public Task UpdateOneAsync(Filter filter, UpdateBuilder update, CommandOptions commandOptions) { @@ -862,6 +1146,7 @@ public Task DeleteOneAsync(Filter filter) } /// + /// /// public Task DeleteOneAsync(Filter filter, CommandOptions commandOptions) { @@ -869,6 +1154,7 @@ public Task DeleteOneAsync(Filter filter, CommandOptions comman } /// + /// /// public Task DeleteOneAsync(TableDeleteOptions deleteOptions, CommandOptions commandOptions) { @@ -876,6 +1162,7 @@ public Task DeleteOneAsync(TableDeleteOptions deleteOptions, Co } /// + /// /// public Task DeleteOneAsync(Filter filter, TableDeleteOptions deleteOptions) { @@ -883,6 +1170,8 @@ public Task DeleteOneAsync(Filter filter, TableDeleteOptions } /// + /// + /// /// public Task DeleteOneAsync(Filter filter, TableDeleteOptions deleteOptions, CommandOptions commandOptions) { @@ -923,6 +1212,7 @@ public Task DeleteManyAsync(Filter filter) } /// + /// /// public Task DeleteManyAsync(Filter filter, CommandOptions commandOptions) { @@ -1090,7 +1380,7 @@ internal Command CreateCommand(string name) return new Command(name, _database.Client, optionsTree, new DatabaseCommandUrlBuilder(_database, _tableName)); } - Task, FindStatusResult>> IQueryRunner>.RunFindManyAsync(Filter filter, IFindManyOptions> findOptions, CommandOptions commandOptions, bool runSynchronously) + Task, FindStatusResult>> IQueryRunner>.RunFindManyAsync(Filter filter, IFindManyOptions> findOptions, CommandOptions commandOptions, bool runSynchronously) where TProjected : class { return RunFindManyAsync(filter, findOptions, commandOptions, runSynchronously); diff --git a/src/DataStax.AstraDB.DataApi/Tables/TableDefinition.cs b/src/DataStax.AstraDB.DataApi/Tables/TableDefinition.cs index bdaa5e9..7f987f4 100644 --- a/src/DataStax.AstraDB.DataApi/Tables/TableDefinition.cs +++ b/src/DataStax.AstraDB.DataApi/Tables/TableDefinition.cs @@ -18,7 +18,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Net; using System.Reflection; using System.Text.Json.Serialization; @@ -34,7 +33,7 @@ public class TableDefinition /// [JsonPropertyName("columns")] [JsonInclude] - public Dictionary Columns { get; set; } = new Dictionary(); + internal Dictionary Columns { get; set; } = new Dictionary(); /// /// The primary key definition for this table @@ -79,7 +78,7 @@ public class TableDefinition var columnNameAttribute = attributes.OfType().FirstOrDefault(); if (columnNameAttribute != null) { - columnName = columnNameAttribute.ColumnName; + columnName = columnNameAttribute.Name; } foreach (var attribute in attributes) @@ -96,29 +95,36 @@ public class TableDefinition case ColumnJsonStringAttribute json: createColumn = false; - definition.AddTextColumn(columnName); + definition.AddColumn(columnName, DataApiType.Text()); break; case ColumnVectorizeAttribute vectorize: createColumn = false; - definition.AddVectorizeColumn( - columnName, - vectorize.Dimension, - vectorize.ServiceProvider, - vectorize.ServiceModelName, - vectorize.AuthenticationPairs?.ToDictionary(s => s.Split('=')[0], s => s.Split('=')[1]), - vectorize.ParameterPairs?.ToDictionary(s => s.Split('=')[0], s => s.Split('=')[1]) - ); + definition.AddColumn(columnName, new VectorizeDataApiType(vectorize.Dimension, new VectorServiceOptions() + { + Provider = vectorize.ServiceProvider, + ModelName = vectorize.ServiceModelName, + Authentication = vectorize.AuthenticationPairs?.ToDictionary(s => s.Split('=')[0], s => s.Split('=')[1]), + Parameters = vectorize.ParameterPairs?.ToDictionary(s => s.Split('=')[0], s => s.Split('=')[1]) + })); + // definition.AddVectorizeColumn( + // columnName, + // vectorize.Dimension, + // vectorize.ServiceProvider, + // vectorize.ServiceModelName, + // vectorize.AuthenticationPairs?.ToDictionary(s => s.Split('=')[0], s => s.Split('=')[1]), + // vectorize.ParameterPairs?.ToDictionary(s => s.Split('=')[0], s => s.Split('=')[1]) + // ); break; case ColumnVectorAttribute vector: if (columnType != typeof(float[]) && columnType != typeof(double[]) && columnType != typeof(string)) { - //TODO: change to check for all numeric array types? throw new InvalidOperationException($"Vector Column {columnName} must be either float[], double[] or string (if sending already binary-encoded string)"); } createColumn = false; - definition.AddVectorColumn(columnName, vector.Dimension); + definition.AddColumn(columnName, DataApiType.Vector(vector.Dimension)); + //definition.AddVectorColumn(columnName, vector.Dimension); break; } } @@ -139,98 +145,10 @@ public class TableDefinition private static void CreateColumnFromPropertyType(string columnName, Type propertyType, TableDefinition definition) { - switch (Type.GetTypeCode(propertyType)) + var type = TypeUtilities.GetDataApiTypeFromUnderlyingType(propertyType); + if (type != null) { - case TypeCode.Int32: - case TypeCode.Int16: - case TypeCode.Byte: - definition.AddIntColumn(columnName); - break; - case TypeCode.String: - definition.AddTextColumn(columnName); - break; - case TypeCode.Boolean: - definition.AddBooleanColumn(columnName); - break; - case TypeCode.DateTime: - definition.AddDateColumn(columnName); - break; - case TypeCode.Decimal: - definition.AddDecimalColumn(columnName); - break; - case TypeCode.Double: - definition.AddDoubleColumn(columnName); - break; - case TypeCode.Int64: - definition.AddLongColumn(columnName); - break; - case TypeCode.Single: - definition.AddFloatColumn(columnName); - break; - case TypeCode.Object: - if (propertyType.IsArray) - { - Type elementType = propertyType.GetElementType(); - if (elementType == typeof(byte)) - { - definition.AddBlobColumn(columnName); - } - //TODO: other array types - } - else if (propertyType.IsEnum) - { - //TODO (int??) - break; - } - else if (propertyType == typeof(Guid)) - { - definition.AddGuidColumn(columnName); - } - else if (propertyType == typeof(Duration)) - { - definition.AddDurationColumn(columnName); - } - else if (propertyType == typeof(IPAddress)) - { - definition.AddIPAddressColumn(columnName); - } - else if (propertyType.IsGenericType) - { - Type genericTypeDefinition = propertyType.GetGenericTypeDefinition(); - Type[] genericArguments = propertyType.GetGenericArguments(); - - if (genericTypeDefinition == typeof(Dictionary<,>)) - { - if (genericArguments.Length == 2 && genericArguments[0] == typeof(string)) - { - definition.AddDictionaryColumn(columnName, genericArguments[0], genericArguments[1]); - } - else - { - Console.WriteLine($"Warning: Unhandled Dictionary type for column: {columnName}. Only string keys are supported."); - } - } - else if (genericTypeDefinition == typeof(List<>)) - { - definition.AddListColumn(columnName, genericArguments[0]); - } - else if (genericTypeDefinition == typeof(HashSet<>)) - { - definition.AddSetColumn(columnName, genericArguments[0]); - } - else - { - Console.WriteLine($"Warning: Unhandled generic type: {propertyType.Name} for column: {columnName}"); - } - } - else - { - Console.WriteLine($"Warning: Unhandled type: {propertyType.Name} for column: {columnName}"); - } - break; - default: - Console.WriteLine($"Warning: Unhandled type code: {Type.GetTypeCode(propertyType)} for column: {columnName}"); - break; + definition.AddColumn(columnName, type); } } @@ -245,24 +163,6 @@ private static void CreateColumnFromPropertyType(string columnName, Type propert return type.Name; } - internal static string GetColumnTypeName(Type type) - { - return Type.GetTypeCode(type) switch - { - TypeCode.Int32 => "int", - TypeCode.String => "text", - TypeCode.Boolean => "boolean", - TypeCode.DateTime => "date", - TypeCode.Decimal => "decimal", - _ => "text", - }; - } - -} - -internal class ColumnTypeConstants -{ - internal const string Text = "text"; } /// @@ -349,264 +249,89 @@ internal static TableDefinition AddCompoundPrimaryKeySort(this TableDefinition t return tableDefinition; } - /// - /// Add a text column to the table definition - /// - /// - /// - /// - public static TableDefinition AddTextColumn(this TableDefinition tableDefinition, string columnName) - { - tableDefinition.Columns.Add(columnName, new TextColumn()); - return tableDefinition; - } - - /// - /// Add an int column to the table definition - /// - /// - /// - /// - public static TableDefinition AddIntColumn(this TableDefinition tableDefinition, string columnName) - { - tableDefinition.Columns.Add(columnName, new IntColumn()); - return tableDefinition; - } - - /// - /// Add a long column to the table definition - /// - /// - /// - /// - public static TableDefinition AddLongColumn(this TableDefinition tableDefinition, string columnName) - { - tableDefinition.Columns.Add(columnName, new LongColumn()); - return tableDefinition; - } - - /// - /// Add a float column to the table definition - /// - /// - /// - /// - public static TableDefinition AddFloatColumn(this TableDefinition tableDefinition, string columnName) - { - tableDefinition.Columns.Add(columnName, new FloatColumn()); - return tableDefinition; - } - - /// - /// Add a boolean column to the table definition - /// - /// - /// - /// - public static TableDefinition AddBooleanColumn(this TableDefinition tableDefinition, string columnName) - { - tableDefinition.Columns.Add(columnName, new BooleanColumn()); - return tableDefinition; - } - - /// - /// Add a date column to the table definition - /// - /// - /// - /// - public static TableDefinition AddDateColumn(this TableDefinition tableDefinition, string columnName) - { - tableDefinition.Columns.Add(columnName, new DateTimeColumn()); - return tableDefinition; - } - - /// - /// Add a vector column to the table definition - /// - /// - /// - /// - /// - public static TableDefinition AddVectorColumn(this TableDefinition tableDefinition, string columnName, int dimension) - { - tableDefinition.Columns.Add(columnName, new VectorColumn(dimension)); - return tableDefinition; - } - - /// - /// Add a vectorize column to the table definition - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static TableDefinition AddVectorizeColumn(this TableDefinition tableDefinition, string columnName, int dimension, string provider, string modelName, Dictionary authentication, Dictionary parameters) - { - tableDefinition.Columns.Add(columnName, new VectorizeColumn(dimension, new VectorServiceOptions - { - Provider = provider, - ModelName = modelName, - Authentication = authentication, - Parameters = parameters - } - )); - return tableDefinition; - } - - /// - /// Add a vectorize column to the table definition - /// - /// - /// - /// - /// - /// - public static TableDefinition AddVectorizeColumn(this TableDefinition tableDefinition, string columnName, int dimension, VectorServiceOptions options) - { - tableDefinition.Columns.Add(columnName, new VectorizeColumn(dimension, options)); - return tableDefinition; - } - - /// - /// Add a vectorize column to the table definition - /// - /// - /// - /// - /// - public static TableDefinition AddVectorizeColumn(this TableDefinition tableDefinition, string columnName, VectorServiceOptions options) - { - tableDefinition.Columns.Add(columnName, new VectorizeColumn(options)); - return tableDefinition; - } - - /// - /// Add a decimal column to the table definition - /// - /// - /// - /// - public static TableDefinition AddDecimalColumn(this TableDefinition tableDefinition, string columnName) - { - tableDefinition.Columns.Add(columnName, new DecimalColumn()); - return tableDefinition; - } - - /// - /// Add a double column to the table definition - /// - /// - /// - /// - public static TableDefinition AddDoubleColumn(this TableDefinition tableDefinition, string columnName) - { - tableDefinition.Columns.Add(columnName, new DoubleColumn()); - return tableDefinition; - } - - /// - /// Add a UUID column to the table definition - /// - /// - /// - /// - public static TableDefinition AddUUIDColumn(this TableDefinition tableDefinition, string columnName) - { - tableDefinition.Columns.Add(columnName, new GuidColumn()); - return tableDefinition; - } - - /// - /// Add a blob column to the table definition - /// - /// - /// - /// - public static TableDefinition AddBlobColumn(this TableDefinition tableDefinition, string columnName) - { - tableDefinition.Columns.Add(columnName, new BlobColumn()); - return tableDefinition; - } - - /// - /// Add a GUID column to the table definition - /// - /// - /// - /// - public static TableDefinition AddGuidColumn(this TableDefinition tableDefinition, string columnName) - { - tableDefinition.Columns.Add(columnName, new GuidColumn()); - return tableDefinition; - } - - /// - /// Add an IP Address column to the table definition - /// - /// - /// - /// - public static TableDefinition AddIPAddressColumn(this TableDefinition tableDefinition, string columnName) - { - tableDefinition.Columns.Add(columnName, new IPAddressColumn()); - return tableDefinition; - } - - /// - /// Add a Duration column to the table definition - /// - /// - /// - /// - public static TableDefinition AddDurationColumn(this TableDefinition tableDefinition, string columnName) - { - tableDefinition.Columns.Add(columnName, new DurationColumn()); - return tableDefinition; - } - - /// - /// Add a dictionary column to the table definition - /// - /// - /// - /// - /// - /// - public static TableDefinition AddDictionaryColumn(this TableDefinition tableDefinition, string columnName, Type keyType, Type valueType) + public static TableDefinition AddColumn(this TableDefinition tableDefinition, string columnName, DataApiType columnType) { - tableDefinition.Columns.Add(columnName, new DictionaryColumn(TableDefinition.GetColumnTypeName(keyType), TableDefinition.GetColumnTypeName(valueType))); + tableDefinition.Columns.Add(columnName, columnType.AsColumnType); return tableDefinition; } - /// - /// Add a set column to the table definition - /// - /// - /// - /// - /// - public static TableDefinition AddSetColumn(this TableDefinition tableDefinition, string columnName, Type valueType) - { - tableDefinition.Columns.Add(columnName, new SetColumn(TableDefinition.GetColumnTypeName(valueType))); - return tableDefinition; - } - - /// - /// Add a list column to the table definition - /// - /// - /// - /// - /// - public static TableDefinition AddListColumn(this TableDefinition tableDefinition, string columnName, Type valueType) - { - tableDefinition.Columns.Add(columnName, new ListColumn(TableDefinition.GetColumnTypeName(valueType))); - return tableDefinition; - } + // /// + // /// Add a text column to the table definition + // /// + // /// + // /// + // /// + // /// (Optional) Type of the keys for this column (e.g. for map columns) + // /// (Optional) Type of the values to be stored in this column (e.g. for list types) + // /// + // public static TableDefinition AddColumn(this TableDefinition tableDefinition, string columnName, DataApiType columnType, DataApiType keyType = default, DataApiType valueType = default) + // { + // tableDefinition.Columns.Add(columnName, new Column() { Type = columnType, KeyType = keyType, ValueType = valueType }); + // return tableDefinition; + // } + + // /// + // /// Add a vector column to the table definition + // /// + // /// + // /// + // /// + // /// + // public static TableDefinition AddVectorColumn(this TableDefinition tableDefinition, string columnName, int dimension) + // { + // tableDefinition.Columns.Add(columnName, new VectorColumn(dimension)); + // return tableDefinition; + // } + + // /// + // /// Add a vectorize column to the table definition + // /// + // /// + // /// + // /// + // /// + // /// + // /// + // /// + // /// + // public static TableDefinition AddVectorizeColumn(this TableDefinition tableDefinition, string columnName, int dimension, string provider, string modelName, Dictionary authentication, Dictionary parameters) + // { + // tableDefinition.Columns.Add(columnName, new VectorizeColumn(dimension, new VectorServiceOptions + // { + // Provider = provider, + // ModelName = modelName, + // Authentication = authentication, + // Parameters = parameters + // } + // )); + // return tableDefinition; + // } + + // /// + // /// Add a vectorize column to the table definition + // /// + // /// + // /// + // /// + // /// + // /// + // public static TableDefinition AddVectorizeColumn(this TableDefinition tableDefinition, string columnName, int dimension, VectorServiceOptions options) + // { + // tableDefinition.Columns.Add(columnName, new VectorizeColumn(dimension, options)); + // return tableDefinition; + // } + + // /// + // /// Add a vectorize column to the table definition + // /// + // /// + // /// + // /// + // /// + // public static TableDefinition AddVectorizeColumn(this TableDefinition tableDefinition, string columnName, VectorServiceOptions options) + // { + // tableDefinition.Columns.Add(columnName, new VectorizeColumn(options)); + // return tableDefinition; + // } } diff --git a/src/DataStax.AstraDB.DataApi/Tables/TableIndex.cs b/src/DataStax.AstraDB.DataApi/Tables/TableIndex.cs index b43d7a2..db930d7 100644 --- a/src/DataStax.AstraDB.DataApi/Tables/TableIndex.cs +++ b/src/DataStax.AstraDB.DataApi/Tables/TableIndex.cs @@ -18,24 +18,14 @@ namespace DataStax.AstraDB.DataApi.Tables; - -public class TableIndex +internal class TableIndex { - /* - { - "name": example_index_name", - "definition": { - "column": "example_column", - "options": { - "caseSensitive": false - } - } -} - */ + [JsonInclude] [JsonPropertyName("name")] - public string IndexName { get; set; } + internal string IndexName { get; set; } + [JsonInclude] [JsonPropertyName("definition")] - public TableIndexDefinition Definition { get; set; } + internal TableIndexDefinition Definition { get; set; } -} \ No newline at end of file +} diff --git a/src/DataStax.AstraDB.DataApi/Tables/TableIndexBuilder.cs b/src/DataStax.AstraDB.DataApi/Tables/TableIndexBuilder.cs new file mode 100644 index 0000000..0cf2d67 --- /dev/null +++ b/src/DataStax.AstraDB.DataApi/Tables/TableIndexBuilder.cs @@ -0,0 +1,103 @@ +/* + * Copyright DataStax, Inc. + * + * 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 + * + * 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. + */ + +using DataStax.AstraDB.DataApi.Core; + +namespace DataStax.AstraDB.DataApi.Tables; + +/// +/// An index builder for tables. +/// +public class TableIndexBuilder +{ + /// + /// Create a default index. + /// + /// + public TableIndexDefinition Index(bool caseSensitive = true, bool normalize = false, bool ascii = false) + { + return new TableIndexDefinition + { + CaseSensitive = caseSensitive, + Normalize = normalize, + Ascii = ascii + }; + } + + /// + /// Create a text index using the default analyzer. + /// + /// + public TableIndexDefinition Text() + { + return new TableTextIndexDefinition(); + } + + /// + /// Create a text index using a specific analyzer. + /// + /// + /// + public TableIndexDefinition Text(TextAnalyzer analyzer) + { + return new TableTextIndexDefinition() + { + Analyzer = analyzer + }; + } + + /// + /// Create a text index using a specific analyzer by name, for example a language-specific analyzer. + /// See https://docs.datastax.com/en/astra-db-serverless/databases/analyzers.html#supported-built-in-analyzers + /// + /// + /// + public TableIndexDefinition Text(string analyzer) + { + return new TableTextIndexDefinition() + { + Analyzer = analyzer + }; + } + + /// + /// Create a text index using custom analyzer options. + /// + /// + /// + public TableIndexDefinition Text(AnalyzerOptions analyzerOptions) + { + return new TableTextIndexDefinition() + { + Analyzer = analyzerOptions + }; + } + + /// + /// Create a vector index. + /// + /// Optional similarity metric to use for vector searches on this index + /// Allows enabling certain vector optimizations on the index by specifying the source model for your vectors + /// + public TableIndexDefinition Vector(SimilarityMetric metric = SimilarityMetric.Cosine, string sourceModel = "other") + { + return new TableVectorIndexDefinition + { + Metric = metric, + SourceModel = sourceModel + }; + } +} diff --git a/src/DataStax.AstraDB.DataApi/Tables/TableIndexDefinition.cs b/src/DataStax.AstraDB.DataApi/Tables/TableIndexDefinition.cs index 9cb0803..bd4207e 100644 --- a/src/DataStax.AstraDB.DataApi/Tables/TableIndexDefinition.cs +++ b/src/DataStax.AstraDB.DataApi/Tables/TableIndexDefinition.cs @@ -23,106 +23,110 @@ namespace DataStax.AstraDB.DataApi.Tables; -public class TableIndexDefinition : TableIndexDefinition -{ - public Expression> Column - { - set - { - ColumnName = value.GetMemberNameTree(); - } - } -} public class TableIndexDefinition { + [JsonInclude] [JsonPropertyName("column")] - public string ColumnName { get; set; } + internal string ColumnName { get; set; } [JsonInclude] [JsonPropertyName("options")] - [JsonConverter(typeof(StringBoolDictionaryConverter))] - internal Dictionary Options { get; set; } = new Dictionary(); + //[JsonConverter(typeof(StringBoolDictionaryConverter))] + internal Dictionary Options { get; set; } = new Dictionary(); [JsonIgnore] public bool CaseSensitive { - get => Options.ContainsKey("caseSensitive") && bool.TryParse(Options["caseSensitive"], out var result) && result; + get => Options.ContainsKey("caseSensitive") && bool.TryParse((string)Options["caseSensitive"], out var result) && result; set => Options["caseSensitive"] = value.ToString().ToLowerInvariant(); } [JsonIgnore] public bool Normalize { - get => Options.ContainsKey("normalize") && bool.TryParse(Options["normalize"], out var result) && result; + get => Options.ContainsKey("normalize") && bool.TryParse((string)Options["normalize"], out var result) && result; set => Options["normalize"] = value.ToString().ToLowerInvariant(); } [JsonIgnore] public bool Ascii { - get => Options.ContainsKey("ascii") && bool.TryParse(Options["ascii"], out var result) && result; + get => Options.ContainsKey("ascii") && bool.TryParse((string)Options["ascii"], out var result) && result; set => Options["ascii"] = value.ToString().ToLowerInvariant(); } -} -public class StringBoolDictionaryConverter : JsonConverter> -{ - public override Dictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.StartObject) - { - throw new JsonException("Expected StartObject token."); - } - - var dictionary = new Dictionary(); - - while (reader.Read()) - { - if (reader.TokenType == JsonTokenType.EndObject) - { - return dictionary; - } - - if (reader.TokenType != JsonTokenType.PropertyName) - { - throw new JsonException("Expected PropertyName token."); - } - - string propertyName = reader.GetString(); - - reader.Read(); // Move to the value - - string value; - switch (reader.TokenType) - { - case JsonTokenType.True: - case JsonTokenType.False: - value = reader.GetBoolean().ToString().ToLowerInvariant(); - break; - case JsonTokenType.String: - value = reader.GetString(); - break; - default: - throw new JsonException($"Unexpected token type {reader.TokenType} for property {propertyName}."); - } - - dictionary[propertyName] = value; - } - - throw new JsonException("Unexpected end of JSON."); - } - - public override void Write(Utf8JsonWriter writer, Dictionary value, JsonSerializerOptions options) - { - writer.WriteStartObject(); - - foreach (var kvp in value) - { - writer.WritePropertyName(kvp.Key); - writer.WriteStringValue(kvp.Value); - } - - writer.WriteEndObject(); - } + internal virtual string IndexCreationCommandName => "createIndex"; } + +// public class TableIndexDefinition : TableIndexDefinition +// { +// public Expression> Column +// { +// set +// { +// ColumnName = value.GetMemberNameTree(); +// } +// } +// } + +// public class StringBoolDictionaryConverter : JsonConverter> +// { +// public override Dictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) +// { +// if (reader.TokenType != JsonTokenType.StartObject) +// { +// throw new JsonException("Expected StartObject token."); +// } + +// var dictionary = new Dictionary(); + +// while (reader.Read()) +// { +// if (reader.TokenType == JsonTokenType.EndObject) +// { +// return dictionary; +// } + +// if (reader.TokenType != JsonTokenType.PropertyName) +// { +// throw new JsonException("Expected PropertyName token."); +// } + +// string propertyName = reader.GetString(); + +// reader.Read(); // Move to the value + +// string value; +// switch (reader.TokenType) +// { +// case JsonTokenType.True: +// case JsonTokenType.False: +// value = reader.GetBoolean().ToString().ToLowerInvariant(); +// break; +// case JsonTokenType.String: +// value = reader.GetString(); +// break; +// default: +// throw new JsonException($"Unexpected token type {reader.TokenType} for property {propertyName}."); +// } + +// dictionary[propertyName] = value; +// } + +// throw new JsonException("Unexpected end of JSON."); +// } + +// public override void Write(Utf8JsonWriter writer, Dictionary value, JsonSerializerOptions options) +// { +// writer.WriteStartObject(); + +// foreach (var kvp in value) +// { +// writer.WritePropertyName(kvp.Key); +// writer.WriteStringValue(kvp.Value); +// } + +// writer.WriteEndObject(); +// } +// } diff --git a/src/DataStax.AstraDB.DataApi/Tables/TableTextIndexDefinition.cs b/src/DataStax.AstraDB.DataApi/Tables/TableTextIndexDefinition.cs new file mode 100644 index 0000000..1612970 --- /dev/null +++ b/src/DataStax.AstraDB.DataApi/Tables/TableTextIndexDefinition.cs @@ -0,0 +1,49 @@ +/* + * Copyright DataStax, Inc. + * + * 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 + * + * 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. + */ + +using DataStax.AstraDB.DataApi.Core; +using DataStax.AstraDB.DataApi.Utils; +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Text.Json.Serialization; + +namespace DataStax.AstraDB.DataApi.Tables; + +public class TableTextIndexDefinition : TableIndexDefinition +{ + + [JsonInclude] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("analyzer")] + internal object Analyzer { get; set; } + + internal override string IndexCreationCommandName => "createTextIndex"; + +} + +// public class TableTextIndexDefinition : TableTextIndexDefinition +// { +// public Expression> Column +// { +// set +// { + +// ColumnName = value.GetMemberNameTree(); +// } +// } + +// } diff --git a/src/DataStax.AstraDB.DataApi/Tables/TableTextIndexTypes.cs b/src/DataStax.AstraDB.DataApi/Tables/TableTextIndexTypes.cs new file mode 100644 index 0000000..e69de29 diff --git a/src/DataStax.AstraDB.DataApi/Tables/TableVectorIndexDefinition.cs b/src/DataStax.AstraDB.DataApi/Tables/TableVectorIndexDefinition.cs index 792c514..af8dae8 100644 --- a/src/DataStax.AstraDB.DataApi/Tables/TableVectorIndexDefinition.cs +++ b/src/DataStax.AstraDB.DataApi/Tables/TableVectorIndexDefinition.cs @@ -23,39 +23,12 @@ namespace DataStax.AstraDB.DataApi.Tables; -/// -/// Definition of a vector index on a table -/// -/// -/// -public class TableVectorIndexDefinition : TableVectorIndexDefinition -{ - /// - /// The column to create the vector index on - /// - public Expression> Column - { - set - { - ColumnName = value.GetMemberNameTree(); - } - } -} /// /// Definition of a vector index on a table /// -public class TableVectorIndexDefinition +public class TableVectorIndexDefinition : TableIndexDefinition { - /// - /// The name of the column to create the vector index on - /// - [JsonPropertyName("column")] - public string ColumnName { get; set; } - - [JsonInclude] - [JsonPropertyName("options")] - internal Dictionary Options { get; set; } = new Dictionary(); /// /// The similarity metric to use @@ -78,4 +51,5 @@ public string SourceModel set { Options["sourceModel"] = value; } } + internal override string IndexCreationCommandName => "createVectorIndex"; } diff --git a/src/DataStax.AstraDB.DataApi/Tables/UserDefinedTypeAttribute.cs b/src/DataStax.AstraDB.DataApi/Tables/UserDefinedTypeAttribute.cs new file mode 100644 index 0000000..2e522d7 --- /dev/null +++ b/src/DataStax.AstraDB.DataApi/Tables/UserDefinedTypeAttribute.cs @@ -0,0 +1,48 @@ +/* + * Copyright DataStax, Inc. + * + * 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 + * + * 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. + */ + +using System; + +namespace DataStax.AstraDB.DataApi.Tables; + +/// +/// Attribute to annotate a class as being a User Defined Type +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] +public class UserDefinedTypeAttribute : Attribute +{ + /// + /// Name of this type (will default to the class name if left null) + /// + public string Name { get; set; } + + /// + /// Construct without a name (will default to class name) + /// + public UserDefinedTypeAttribute() + { + } + + /// + /// Construct and specify the name to use for the User Defined Type + /// + /// + public UserDefinedTypeAttribute(string name) + { + Name = name; + } +} + diff --git a/src/DataStax.AstraDB.DataApi/Tables/UserDefinedTypeDefinition.cs b/src/DataStax.AstraDB.DataApi/Tables/UserDefinedTypeDefinition.cs new file mode 100644 index 0000000..5d43de4 --- /dev/null +++ b/src/DataStax.AstraDB.DataApi/Tables/UserDefinedTypeDefinition.cs @@ -0,0 +1,129 @@ +/* + * Copyright DataStax, Inc. + * + * 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 + * + * 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. + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.Json.Serialization; + +namespace DataStax.AstraDB.DataApi.Tables; + +/// +/// Options for creating a User Defined Type +/// +public class UserDefinedTypeDefinition +{ + [JsonPropertyName("fields")] + [JsonInclude] + private Dictionary FieldTypes => Fields.ToDictionary(x => x.Key, x => x.Value.Key.ToLowerInvariant()); + + /// + /// List of fields (field name, field type) for this User Defined Type + /// + [JsonIgnore] + public Dictionary Fields { get; set; } = new Dictionary(); + +} + + +internal class UserDefinedTypeRequest +{ + [JsonInclude] + [JsonPropertyName("name")] + internal string Name { get; set; } + + [JsonInclude] + [JsonPropertyName("definition")] + internal UserDefinedTypeDefinition TypeDefinition { get; set; } + + [JsonInclude] + [JsonPropertyName("options")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + internal Dictionary Options { get; set; } + + internal void SetSkipIfExists(bool skipIfExists) + { + var optionsKey = "ifNotExists"; + if (!skipIfExists) + { + if (Options != null) + { + Options.Remove(optionsKey); + } + } + else + { + Options ??= new Dictionary(); + Options[optionsKey] = skipIfExists; + } + } + + internal static UserDefinedTypeDefinition CreateDefinitionFromType() + { + return CreateDefinitionFromType(typeof(T)); + } + + internal static UserDefinedTypeDefinition CreateDefinitionFromType(Type wrapperType) + { + UserDefinedTypeDefinition definition = new(); + + foreach (var property in wrapperType.GetProperties()) + { + var propertyType = property.PropertyType; + var typeInfo = TypeUtilities.GetDataApiType(propertyType); + if (typeInfo.IsSimpleType == false) + { + throw new ArgumentException($"Property '{property.Name}' in type '{wrapperType.Name}' is not a supported simple type for UDTs."); + } + definition.Fields.Add(GetColumnName(property), typeInfo); + } + + return definition; + } + + internal static string GetUserDefinedTypeName() + { + return GetUserDefinedTypeName(typeof(T)); + } + + internal static string GetUserDefinedTypeName(Type type) + { + var nameAttribute = type.GetCustomAttribute(); + return GetUserDefinedTypeName(type, nameAttribute); + } + + internal static string GetUserDefinedTypeName(Type type, UserDefinedTypeAttribute nameAttribute) + { + if (nameAttribute != null && nameAttribute.Name != null) + { + return nameAttribute.Name; + } + return type.Name; + } + + internal static string GetColumnName(PropertyInfo property) + { + var nameAttribute = property.GetCustomAttribute(); + if (nameAttribute != null) + { + return nameAttribute.Name; + } + return property.Name; + } +} + + diff --git a/src/DataStax.AstraDB.DataApi/Tables/UserDefinedTypeInfo.cs b/src/DataStax.AstraDB.DataApi/Tables/UserDefinedTypeInfo.cs new file mode 100644 index 0000000..8cd8333 --- /dev/null +++ b/src/DataStax.AstraDB.DataApi/Tables/UserDefinedTypeInfo.cs @@ -0,0 +1,133 @@ +/* + * Copyright DataStax, Inc. + * + * 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 + * + * 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. + */ + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace DataStax.AstraDB.DataApi.Tables; + +/// +/// Description of an existing User Defined Type +/// +public class UserDefinedTypeInfo +{ + /// + /// The type + /// + [JsonPropertyName("type")] + public string Type { get; set; } + + /// + /// Name + /// + [JsonPropertyName("udtName")] + public string Name { get; set; } + + /// + /// List of fields for this User Defined Type + /// + [JsonPropertyName("definition")] + public TypeDefinitionInfo Definition { get; set; } + + /// + /// Information regarding API support for this Type + /// + [JsonPropertyName("apiSupport")] + public TypeApiSupportInfo ApiSupport { get; set; } +} + +/* +{ + "type": "userDefined", + "udtName": "address", + "definition": { + "fields": { + "city": { + "type": "text" + }, + "country": { + "type": "text" + } + } + }, + "apiSupport": { + "createTable": true, + "insert": true, + "read": true, + "filter": false, + "cqlDefinition": "demo.address" + } +*/ + +/// +/// Information regarding API support for a User Defined Type +/// +public class TypeApiSupportInfo +{ + /// + /// Can a table be created using this Type? + /// + [JsonPropertyName("createTable")] + public bool CanCreateTable { get; set; } + + /// + /// Can data be inserted using this Type? + /// + [JsonPropertyName("insert")] + public bool CanInsert { get; set; } + + /// + /// Can this type be read from the DB? + /// + [JsonPropertyName("read")] + public bool CanRead { get; set; } + + /// + /// Can this type be used for filtering? + /// + [JsonPropertyName("filter")] + public bool CanFilter { get; set; } + + /// + /// CqlDefinition for this Type + /// + [JsonPropertyName("cqlDefinition")] + public string CqlDefinition { get; set; } +} + +/// +/// List of Fields for a User Defined Type +/// +public class TypeDefinitionInfo +{ + /// + /// Field types, by column name + /// + [JsonPropertyName("fields")] + public Dictionary Fields { get; set; } +} + +/// +/// DataApi Type for a specific field +/// +public class FieldTypeInfo +{ + /// + /// The type :) + /// + [JsonPropertyName("type")] + public string Type { get; set; } +} \ No newline at end of file diff --git a/src/DataStax.AstraDB.DataApi/Tables/UserDefinedTypeNameAttribute.cs b/src/DataStax.AstraDB.DataApi/Tables/UserDefinedTypeNameAttribute.cs new file mode 100644 index 0000000..2652975 --- /dev/null +++ b/src/DataStax.AstraDB.DataApi/Tables/UserDefinedTypeNameAttribute.cs @@ -0,0 +1,40 @@ +// /* +// * Copyright DataStax, Inc. +// * +// * 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 +// * +// * 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. +// */ + +// using System; + +// namespace DataStax.AstraDB.DataApi.Tables; + +// /// +// /// Attribute used to specify the name to use for a User Defined Type +// /// +// [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] +// public class UserDefinedTypeNameAttribute : Attribute +// { +// /// +// /// The name :) +// /// +// public string Name { get; set; } + +// /// +// /// Define the name to use for a User Defined Type +// /// +// /// +// public UserDefinedTypeNameAttribute(string name) +// { +// Name = name; +// } +// } \ No newline at end of file diff --git a/src/DataStax.AstraDB.DataApi/Utils/Extensions.cs b/src/DataStax.AstraDB.DataApi/Utils/Extensions.cs index ba02862..972be1b 100644 --- a/src/DataStax.AstraDB.DataApi/Utils/Extensions.cs +++ b/src/DataStax.AstraDB.DataApi/Utils/Extensions.cs @@ -14,11 +14,13 @@ * limitations under the License. */ +using DataStax.AstraDB.DataApi.Core; using DataStax.AstraDB.DataApi.Core.Commands; using DataStax.AstraDB.DataApi.SerDes; using DataStax.AstraDB.DataApi.Tables; using System; using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Text; @@ -30,6 +32,33 @@ namespace DataStax.AstraDB.DataApi.Utils; internal static class Extensions { + internal static string ToUrlString(this ApiVersion apiVersion) + { + return apiVersion switch + { + ApiVersion.V1 => "v1", + _ => "v1", + }; + } + + internal static TResult ResultSync(this Task task) + { + return task.GetAwaiter().GetResult(); + } + + internal static void ResultSync(this Task task) + { + task.GetAwaiter().GetResult(); + } + + internal static IEnumerable> CreateBatch(this IEnumerable list, int chunkSize) + { + for (int i = 0; i < list.Count(); i += chunkSize) + { + yield return list.Skip(i).Take(Math.Min(chunkSize, list.Count() - i)); + } + } + internal static string OrIfEmpty(this string str, string alternate) { return string.IsNullOrEmpty(str) ? alternate : str; @@ -99,7 +128,7 @@ private static void BuildPropertyName(MemberExpression memberExpression, StringB var columnNameAttribute = propertyInfo.GetCustomAttribute(); if (columnNameAttribute != null) { - name = columnNameAttribute.ColumnName; + name = columnNameAttribute.Name; } } sb.Append(name); diff --git a/src/DataStax.AstraDB.DataApi/Utils/TypeUtilities.cs b/src/DataStax.AstraDB.DataApi/Utils/TypeUtilities.cs new file mode 100644 index 0000000..fee995d --- /dev/null +++ b/src/DataStax.AstraDB.DataApi/Utils/TypeUtilities.cs @@ -0,0 +1,361 @@ + + +using DataStax.AstraDB.DataApi.Core; +using DataStax.AstraDB.DataApi.Tables; +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Net; +using System.Reflection; +using System.Text.Json.Serialization; + +public class TypeUtilities +{ + internal static Type GetUnderlyingType(Type propertyType, int dictionaryPosition = 1) + { + if (propertyType.IsGenericType) + { + if (propertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + return Nullable.GetUnderlyingType(propertyType); + } + Type genericTypeDefinition = propertyType.GetGenericTypeDefinition(); + Type[] genericArguments = propertyType.GetGenericArguments(); + + if (genericArguments.Length > 1) + { + return genericArguments[dictionaryPosition]; + } + + return genericArguments[0]; + } + if (propertyType.IsArray) + { + return propertyType.GetElementType(); + } + return propertyType; + } + + public static DataApiType GetDataApiType(Type propertyType) + { + Type underlyingType; + if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + underlyingType = Nullable.GetUnderlyingType(propertyType); + } + else + { + underlyingType = propertyType; + } + return GetDataApiTypeFromUnderlyingType(underlyingType); + } + + public static DataApiType GetDataApiTypeFromUnderlyingType(Type propertyType) + { + DataApiType type = null; + switch (Type.GetTypeCode(propertyType)) + { + case TypeCode.Int32: + case TypeCode.Int16: + case TypeCode.Byte: + return DataApiType.Int(); + case TypeCode.String: + return DataApiType.Text(); + case TypeCode.Boolean: + return DataApiType.Boolean(); + case TypeCode.DateTime: + return DataApiType.Timestamp(); + case TypeCode.Decimal: + return DataApiType.Decimal(); + case TypeCode.Double: + return DataApiType.Double(); + case TypeCode.Int64: + return DataApiType.BigInt(); + case TypeCode.Single: + return DataApiType.Float(); + case TypeCode.Object: + if (propertyType.FullName == "System.DateOnly") + { + return DataApiType.Date(); + } + else if (propertyType.FullName == "System.TimeOnly") + { + return DataApiType.Time(); + } + else if (propertyType.IsArray) + { + Type elementType = propertyType.GetElementType(); + if (elementType == typeof(byte)) + { + return DataApiType.Blob(); + } + else + { + return DataApiType.List(GetDataApiTypeFromUnderlyingType(elementType)); + } + } + else if (propertyType.IsEnum) + { + //skip + Console.WriteLine($"Enum types are not currently supported for column. Consider using a string or int property instead."); + } + else if (propertyType == typeof(Guid)) + { + return DataApiType.Uuid(); + } + else if (propertyType == typeof(Duration)) + { + return DataApiType.Duration(); + } + else if (propertyType == typeof(IPAddress)) + { + return DataApiType.Inet(); + } + else if (propertyType.IsGenericType) + { + Type genericTypeDefinition = propertyType.GetGenericTypeDefinition(); + Type[] genericArguments = propertyType.GetGenericArguments(); + + if (genericTypeDefinition == typeof(Dictionary<,>)) + { + if (genericArguments.Length == 2 && genericArguments[0] == typeof(string)) + { + return DataApiType.Map(GetDataApiTypeFromUnderlyingType(genericArguments[1])); + } + else + { + Console.WriteLine($"Warning: Unhandled Dictionary type. Only string keys are supported."); + } + } + else if (genericTypeDefinition == typeof(List<>)) + { + return DataApiType.List(GetDataApiTypeFromUnderlyingType(genericArguments[0])); + } + else if (genericTypeDefinition == typeof(HashSet<>)) + { + return DataApiType.Set(GetDataApiTypeFromUnderlyingType(genericArguments[0])); + } + else + { + //skip + Console.WriteLine($"Warning: Unhandled generic type: {propertyType.Name}"); + } + } + else + { + var attribute = propertyType.GetCustomAttribute(); + if (attribute != null) + { + var typeName = UserDefinedTypeRequest.GetUserDefinedTypeName(propertyType, attribute); + return DataApiType.UserDefined(typeName); + } + //skip + Console.WriteLine($"Warning: Unhandled type: {propertyType.Name}"); + } + break; + default: + //skip + Console.WriteLine($"Warning: Unhandled type code: {Type.GetTypeCode(propertyType)}"); + break; + } + return type; + } + + internal static IEnumerable FindPropertiesWithUserDefinedTypeAttribute(Type type) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); + + foreach (var property in properties) + { + var attribute = property.PropertyType.GetCustomAttribute(); + if (attribute != null) + { + yield return new UserDefinedProperty() { Property = property, Attribute = attribute, UnderlyingType = GetUnderlyingType(property.PropertyType) }; + continue; + } + + Type elementType = GetCollectionElementType(property.PropertyType); + attribute = elementType == null ? null : elementType.GetCustomAttribute(); + if (elementType != null && attribute != null) + { + yield return new UserDefinedProperty() { Property = property, Attribute = attribute, UnderlyingType = elementType }; + } + } + } + + private static Type GetCollectionElementType(Type type) + { + if (type != typeof(string) && typeof(IEnumerable).IsAssignableFrom(type)) + { + if (type.IsGenericType) + { + var genericType = type.GetGenericTypeDefinition(); + if (genericType == typeof(List<>) || + genericType == typeof(HashSet<>) || + genericType == typeof(Dictionary<,>) || + genericType == typeof(IEnumerable<>) || + genericType == typeof(ICollection<>) || + genericType == typeof(IList<>)) + { + if (genericType == typeof(Dictionary<,>)) + { + return type.GetGenericArguments()[1]; // Value type + } + return type.GetGenericArguments()[0]; + } + } + else if (type.IsArray) + { + return type.GetElementType(); + } + } + return null; + } + +} + +internal class UserDefinedProperty +{ + public PropertyInfo Property { get; set; } + public UserDefinedTypeAttribute Attribute { get; set; } + public Type UnderlyingType { get; set; } +} + +public class DataApiType +{ + public DataApiType(string key) + { + Key = key; + } + + [JsonPropertyName("type")] + public string Key { get; set; } + + internal virtual object AsValueType => Key; + + internal virtual object AsColumnType => this; + + internal virtual bool IsSimpleType => true; + + public static DataApiType Ascii() => new DataApiType("ascii"); + public static DataApiType BigInt() => new DataApiType("bigint"); + public static DataApiType Blob() => new DataApiType("blob"); + public static DataApiType Boolean() => new DataApiType("boolean"); + public static DataApiType Date() => new DataApiType("date"); + public static DataApiType Decimal() => new DataApiType("decimal"); + public static DataApiType Double() => new DataApiType("double"); + public static DataApiType Duration() => new DataApiType("duration"); + public static DataApiType Float() => new DataApiType("float"); + public static DataApiType Inet() => new DataApiType("inet"); + public static DataApiType Int() => new DataApiType("int"); + + public static DataApiType Text() => new DataApiType("text"); + public static DataApiType Time() => new DataApiType("time"); + public static DataApiType Timestamp() => new DataApiType("timestamp"); + public static DataApiType Uuid() => new DataApiType("uuid"); + + public static DataApiType List(DataApiType valueType) => new ListDataApiType(valueType); + public static DataApiType Map(DataApiType valueType) => new MapDataApiType(valueType); + public static DataApiType Set(DataApiType valueType) => new ListDataApiType("set", valueType); + public static DataApiType Vector(int dimension) => new VectorDataApiType(dimension); + public static DataApiType Vectorize(int dimensions, VectorServiceOptions serviceOptions) => new VectorizeDataApiType(dimensions, serviceOptions); + public static DataApiType UserDefined(string name) => new UserDefinedDataApiType(name); +} + +public class VectorDataApiType : DataApiType +{ + /// + /// The dimension of the vector + /// + [JsonPropertyName("dimension")] + public int Dimension { get; set; } + + public VectorDataApiType(int dimension) : base("vector") + { + Dimension = dimension; + } + + internal override object AsValueType => this; + + internal override object AsColumnType => this; + + internal override bool IsSimpleType => false; +} + +public class VectorizeDataApiType : VectorDataApiType +{ + /// + /// The vectorization service options + /// + [JsonPropertyName("service")] + public VectorServiceOptions ServiceOptions { get; set; } + + public VectorizeDataApiType(int dimensions, VectorServiceOptions serviceOptions) : base(dimensions) + { + ServiceOptions = serviceOptions; + } + + internal override object AsValueType => this; + + internal override object AsColumnType => this; + +} + +public class UserDefinedDataApiType : DataApiType +{ + [JsonPropertyName("udtName")] + public string UserDefinedTypeName { get; set; } + + public UserDefinedDataApiType(string name) : base("userDefined") + { + UserDefinedTypeName = name; + } + + internal override object AsValueType => this; + + internal override object AsColumnType => this; + + internal override bool IsSimpleType => false; +} + +public class ListDataApiType : DataApiType +{ + [JsonInclude] + [JsonPropertyName("valueType")] + internal object ValueTypeObject => ValueType.AsValueType; + + [JsonIgnore] + public DataApiType ValueType { get; set; } + + public ListDataApiType(DataApiType valueType) : base("list") + { + ValueType = valueType; + } + + public ListDataApiType(string baseType, DataApiType valueType) : base(baseType) + { + ValueType = valueType; + } + + internal override object AsValueType => this; + + internal override object AsColumnType => this; + + internal override bool IsSimpleType => false; + +} + +public class MapDataApiType : ListDataApiType +{ + [JsonInclude] + [JsonPropertyName("keyType")] + internal object KeyType => "text"; + + public MapDataApiType(DataApiType valueType) : base("map", valueType) + { + } +} diff --git a/test/DataStax.AstraDB.DataApi.IntegrationTests/AssemblyInfo.cs b/test/DataStax.AstraDB.DataApi.IntegrationTests/AssemblyInfo.cs index d296ff0..bc1b946 100644 --- a/test/DataStax.AstraDB.DataApi.IntegrationTests/AssemblyInfo.cs +++ b/test/DataStax.AstraDB.DataApi.IntegrationTests/AssemblyInfo.cs @@ -1,4 +1,4 @@ using Xunit; -[assembly: CollectionBehavior(MaxParallelThreads = 4)] +[assembly: CollectionBehavior(DisableTestParallelization = true)] [assembly: AssemblyFixture(typeof(DataStax.AstraDB.DataApi.IntegrationTests.Fixtures.AssemblyFixture))] diff --git a/test/DataStax.AstraDB.DataApi.IntegrationTests/DataStax.AstraDB.DataApi.IntegrationTests.csproj b/test/DataStax.AstraDB.DataApi.IntegrationTests/DataStax.AstraDB.DataApi.IntegrationTests.csproj index 4454d78..1c95307 100644 --- a/test/DataStax.AstraDB.DataApi.IntegrationTests/DataStax.AstraDB.DataApi.IntegrationTests.csproj +++ b/test/DataStax.AstraDB.DataApi.IntegrationTests/DataStax.AstraDB.DataApi.IntegrationTests.csproj @@ -23,5 +23,6 @@ + diff --git a/test/DataStax.AstraDB.DataApi.IntegrationTests/Fixtures/TableIndexesFixture.cs b/test/DataStax.AstraDB.DataApi.IntegrationTests/Fixtures/TableIndexesFixture.cs index 82e141a..d6f6d1c 100644 --- a/test/DataStax.AstraDB.DataApi.IntegrationTests/Fixtures/TableIndexesFixture.cs +++ b/test/DataStax.AstraDB.DataApi.IntegrationTests/Fixtures/TableIndexesFixture.cs @@ -29,17 +29,17 @@ public TableIndexesFixture(AssemblyFixture assemblyFixture) : base(assemblyFixtu public async ValueTask InitializeAsync() { - await CreateTestTable(); + } public async ValueTask DisposeAsync() { - await Database.DropTableAsync(_fixtureTableName); + } - private const string _fixtureTableName = "tableIndexesTest"; - private async Task CreateTestTable() + public static async Task AddTableRows(Table table) { + var startDate = DateTime.UtcNow.Date.AddDays(7); var eventRows = new List @@ -70,11 +70,8 @@ private async Task CreateTestTable() } }; + return await table.InsertManyAsync(eventRows); - var table = await Database.CreateTableAsync(_fixtureTableName); - await table.InsertManyAsync(eventRows); - - FixtureTestTable = table; } } \ No newline at end of file diff --git a/test/DataStax.AstraDB.DataApi.IntegrationTests/Fixtures/TablesFixture.cs b/test/DataStax.AstraDB.DataApi.IntegrationTests/Fixtures/TablesFixture.cs index c67786a..f16cb7e 100644 --- a/test/DataStax.AstraDB.DataApi.IntegrationTests/Fixtures/TablesFixture.cs +++ b/test/DataStax.AstraDB.DataApi.IntegrationTests/Fixtures/TablesFixture.cs @@ -1,5 +1,7 @@ using DataStax.AstraDB.DataApi.Core; using DataStax.AstraDB.DataApi.Tables; +using System.ComponentModel.DataAnnotations; +using System.Numerics; using Xunit; namespace DataStax.AstraDB.DataApi.IntegrationTests.Fixtures; @@ -83,31 +85,34 @@ private async Task CreateSearchTable() rows.Add(row); } var table = await Database.CreateTableAsync(_queryTableName); - await table.CreateIndexAsync(new TableIndex() - { - IndexName = "number_of_pages_index", - Definition = new TableIndexDefinition() - { - Column = (b) => b.NumberOfPages - } - }); - await table.CreateVectorIndexAsync(new TableVectorIndex() - { - IndexName = "author_index", - Definition = new TableVectorIndexDefinition() - { - Column = (b) => b.Author, - Metric = SimilarityMetric.Cosine, - } - }); - await table.CreateIndexAsync(new TableIndex() - { - IndexName = "due_date_index", - Definition = new TableIndexDefinition() - { - Column = (b) => b.DueDate - } - }); + // await table.CreateIndexAsync(new TableIndex() + // { + // IndexName = "number_of_pages_index", + // Definition = new TableIndexDefinition() + // { + // Column = (b) => b.NumberOfPages + // } + // }); + await table.CreateIndexAsync((b) => b.NumberOfPages); + // await table.CreateVectorIndexAsync(new TableVectorIndex() + // { + // IndexName = "author_index", + // Definition = new TableVectorIndexDefinition() + // { + // Column = (b) => b.Author, + // Metric = SimilarityMetric.Cosine, + // } + // }); + await table.CreateIndexAsync((b) => b.Author, Builders.TableIndex.Vector(SimilarityMetric.Cosine)); + // await table.CreateIndexAsync(new TableIndex() + // { + // IndexName = "due_date_index", + // Definition = new TableIndexDefinition() + // { + // Column = (b) => b.DueDate + // } + // }); + await table.CreateIndexAsync((b) => b.DueDate); await table.InsertManyAsync(rows); SearchTable = table; } @@ -121,104 +126,84 @@ private async Task CreateTestTablesNotTyped() { // Create a table with a single primary key var createDefinition = new TableDefinition() - .AddIntColumn("Id") - .AddTextColumn("IdTwo") - .AddTextColumn("Name") - .AddTextColumn("SortOneAscending") - .AddTextColumn("SortTwoDescending") - .AddVectorizeColumn("Vectorize", 1024, new VectorServiceOptions + .AddColumn("Id", DataApiType.Int()) + .AddColumn("IdTwo", DataApiType.Text()) + .AddColumn("Name", DataApiType.Text()) + .AddColumn("SortOneAscending", DataApiType.Text()) + .AddColumn("SortTwoDescending", DataApiType.Text()) + .AddColumn("Vectorize", DataApiType.Vectorize(1024, new VectorServiceOptions { Provider = "nvidia", ModelName = "NV-Embed-QA" - }) - .AddVectorColumn("Vector", 384) + })) + .AddColumn("Vector", DataApiType.Vector(384)) .AddSinglePrimaryKey("Id"); + /* + CHANGED FROM THIS: + .AddVectorizeColumn("Vectorize", 1024, new VectorServiceOptions + { + Provider = "nvidia", + ModelName = "NV-Embed-QA" + }) + TO THIS: + .AddColumn("Vectorize", DataApiType.Vectorize(1024, new VectorServiceOptions + { + Provider = "nvidia", + ModelName = "NV-Embed-QA" + })) + + CHANGED FROM THIS: + .AddVectorColumn("Vector", 384) + + TO THIS: + .AddColumn("Vector", DataApiType.Vector(384)) + + */ + UntypedTableSinglePrimaryKey = await Database.CreateTableAsync(_untypedSinglePkTableName, createDefinition); - await UntypedTableSinglePrimaryKey.CreateVectorIndexAsync(new TableVectorIndex() - { - IndexName = "vectorize_index", - Definition = new TableVectorIndexDefinition() - { - ColumnName = "Vectorize", - } - }); - await UntypedTableSinglePrimaryKey.CreateVectorIndexAsync(new TableVectorIndex() - { - IndexName = "vector_index", - Definition = new TableVectorIndexDefinition() - { - ColumnName = "Vector", - } - }); + await UntypedTableSinglePrimaryKey.CreateIndexAsync("vectorize_index", "Vectorize", Builders.TableIndex.Vector()); + await UntypedTableSinglePrimaryKey.CreateIndexAsync("vector_index", "Vector", Builders.TableIndex.Vector()); // Create a table with a composite primary key createDefinition = new TableDefinition() - .AddIntColumn("Id") - .AddTextColumn("IdTwo") - .AddTextColumn("Name") - .AddTextColumn("SortOneAscending") - .AddTextColumn("SortTwoDescending") - .AddVectorizeColumn("Vectorize", 1024, new VectorServiceOptions + .AddColumn("Id", DataApiType.Int()) + .AddColumn("IdTwo", DataApiType.Text()) + .AddColumn("Name", DataApiType.Text()) + .AddColumn("SortOneAscending", DataApiType.Text()) + .AddColumn("SortTwoDescending", DataApiType.Text()) + .AddColumn("Vectorize", DataApiType.Vectorize(1024, new VectorServiceOptions { Provider = "nvidia", ModelName = "NV-Embed-QA" - }) - .AddVectorColumn("Vector", 384) + })) + .AddColumn("Vector", DataApiType.Vector(384)) .AddCompositePrimaryKey(new[] { "Id", "IdTwo" }); UntypedTableCompositePrimaryKey = await Database.CreateTableAsync(_untypedCompositePkTableName, createDefinition); - await UntypedTableCompositePrimaryKey.CreateVectorIndexAsync(new TableVectorIndex() - { - IndexName = "composite_vectorize_index", - Definition = new TableVectorIndexDefinition() - { - ColumnName = "Vectorize", - } - }); - await UntypedTableCompositePrimaryKey.CreateVectorIndexAsync(new TableVectorIndex() - { - IndexName = "composite_vector_index", - Definition = new TableVectorIndexDefinition() - { - ColumnName = "Vector", - } - }); + await UntypedTableCompositePrimaryKey.CreateIndexAsync("composite_vectorize_index", "Vectorize", Builders.TableIndex.Vector()); + await UntypedTableCompositePrimaryKey.CreateIndexAsync("composite_vector_index", "Vector", Builders.TableIndex.Vector()); // Create a table with a compound primary key createDefinition = new TableDefinition() - .AddIntColumn("Id") - .AddTextColumn("IdTwo") - .AddTextColumn("Name") - .AddTextColumn("SortOneAscending") - .AddTextColumn("SortTwoDescending") - .AddVectorizeColumn("Vectorize", 1024, new VectorServiceOptions + .AddColumn("Id", DataApiType.Int()) + .AddColumn("IdTwo", DataApiType.Text()) + .AddColumn("Name", DataApiType.Text()) + .AddColumn("SortOneAscending", DataApiType.Text()) + .AddColumn("SortTwoDescending", DataApiType.Text()) + .AddColumn("Vectorize", DataApiType.Vectorize(1024, new VectorServiceOptions { Provider = "nvidia", ModelName = "NV-Embed-QA" - }) - .AddVectorColumn("Vector", 384) + })) + .AddColumn("Vector", DataApiType.Vector(384)) .AddCompoundPrimaryKey(new[] { "Id", "IdTwo" }, new[] { new PrimaryKeySort("SortOneAscending", SortDirection.Ascending), new PrimaryKeySort("SortTwoDescending", SortDirection.Descending) }); UntypedTableCompoundPrimaryKey = await Database.CreateTableAsync(_untypedCompoundPkTableName, createDefinition); - await UntypedTableCompoundPrimaryKey.CreateVectorIndexAsync(new TableVectorIndex() - { - IndexName = "compound_vectorize_index", - Definition = new TableVectorIndexDefinition() - { - ColumnName = "Vectorize", - } - }); - await UntypedTableCompoundPrimaryKey.CreateVectorIndexAsync(new TableVectorIndex() - { - IndexName = "compound_vector_index", - Definition = new TableVectorIndexDefinition() - { - ColumnName = "Vector", - } - }); + await UntypedTableCompoundPrimaryKey.CreateIndexAsync("compound_vectorize_index", "Vectorize", Builders.TableIndex.Vector()); + await UntypedTableCompoundPrimaryKey.CreateIndexAsync("compound_vector_index", "Vector", Builders.TableIndex.Vector()); // Populate untyped tables with sample data List rows = new List(); @@ -275,30 +260,10 @@ private async Task CreateDeleteTable() rows.Add(row); } var table = await Database.CreateTableAsync(_deleteTableName); - await table.CreateIndexAsync(new TableIndex() - { - IndexName = "delete_table_number_of_pages_index", - Definition = new TableIndexDefinition() - { - Column = (b) => b.NumberOfPages - } - }); - await table.CreateVectorIndexAsync(new TableVectorIndex() - { - IndexName = "delete_table_author_index", - Definition = new TableVectorIndexDefinition() - { - Column = (b) => b.Author - } - }); - await table.CreateIndexAsync(new TableIndex() - { - IndexName = "delete_table_due_date_index", - Definition = new TableIndexDefinition() - { - Column = (b) => b.DueDate - } - }); + await table.CreateIndexAsync("delete_table_number_of_pages_index", "NumberOfPages"); + await table.CreateIndexAsync("delete_table_author_vector_index", (b) => b.Author, Builders.TableIndex.Vector()); + await table.CreateIndexAsync("delete_table_due_date_index", (b) => b.DueDate); + await table.InsertManyAsync(rows); DeleteTable = table; } diff --git a/test/DataStax.AstraDB.DataApi.IntegrationTests/Fixtures/UserDefinedTypesFixture.cs b/test/DataStax.AstraDB.DataApi.IntegrationTests/Fixtures/UserDefinedTypesFixture.cs new file mode 100644 index 0000000..64f07fd --- /dev/null +++ b/test/DataStax.AstraDB.DataApi.IntegrationTests/Fixtures/UserDefinedTypesFixture.cs @@ -0,0 +1,29 @@ +using DataStax.AstraDB.DataApi.Core; +using DataStax.AstraDB.DataApi.Tables; +using Xunit; + +namespace DataStax.AstraDB.DataApi.IntegrationTests.Fixtures; + +[CollectionDefinition("UserDefinedTypes")] +public class UserDefinedTypesCollection : ICollectionFixture, ICollectionFixture +{ + +} + +public class UserDefinedTypesFixture : BaseFixture +{ + public UserDefinedTypesFixture(AssemblyFixture assemblyFixture) : base(assemblyFixture, "userDefinedTypes") + { + try + { + var keyspaces = Database.GetAdmin().ListKeyspaces(); + Console.WriteLine($"[Fixture] Connected. Keyspaces found: {keyspaces.Count()}"); + } + catch (Exception ex) + { + Console.WriteLine($"[Fixture] Connection failed: {ex.Message}"); + throw; + } + } + +} \ No newline at end of file diff --git a/test/DataStax.AstraDB.DataApi.IntegrationTests/TestObjects.cs b/test/DataStax.AstraDB.DataApi.IntegrationTests/TestObjects.cs index fb35eb4..896a71a 100644 --- a/test/DataStax.AstraDB.DataApi.IntegrationTests/TestObjects.cs +++ b/test/DataStax.AstraDB.DataApi.IntegrationTests/TestObjects.cs @@ -24,6 +24,21 @@ public class SimpleObjectWithVectorize public string StringToVectorize => Name; } +public class SimpleObjectWithLexical +{ + [DocumentId] + [ColumnPrimaryKey] + public int? Id { get; set; } + public string Name { get; set; } + [DocumentMapping(DocumentMappingField.Lexical)] + [LexicalOptions( + TokenizerName = "standard", + Filters = new[] { "lowercase", "stop", "porterstem", "asciifolding" }, + CharacterFilters = new string[] { } + )] + public string LexicalValue => Name; +} + public class SimpleObjectWithVectorizeResult : SimpleObjectWithVectorize { [DocumentMapping(DocumentMappingField.Similarity)] @@ -225,6 +240,14 @@ public class RowTestObject public Duration Duration { get; set; } } +[TableName("testTable")] +public class ArrayTestRow +{ + [ColumnPrimaryKey(1)] + public int Id { get; set; } + public string[]? StringArray { get; set; } +} + public class CompositePrimaryKey { [ColumnPrimaryKey(2)] @@ -290,4 +313,124 @@ public class Book [DocumentMapping(DocumentMappingField.Vectorize)] public string? StringToVectorize { get; set; } +} + + +public class TestDataBook +{ + // This table uses a composite primary key + // with 'title' as the first column in the key + [ColumnPrimaryKey(1)] + [ColumnName("title")] + public string Title { get; set; } = null!; + + // This table uses a composite primary key + // with 'author' as the second column in the key + [ColumnPrimaryKey(2)] + [ColumnName("author")] + public string Author { get; set; } = null!; + + [ColumnName("number_of_pages")] + public int? NumberOfPages { get; set; } + + [ColumnName("rating")] + public float? Rating { get; set; } + + [ColumnName("publication_year")] + public int? PublicationYear { get; set; } + + [ColumnName("summary")] + public string? Summary { get; set; } + + [ColumnName("genres")] + public HashSet? Genres { get; set; } + + [ColumnName("metadata")] + public Dictionary? Metadata { get; set; } + + [ColumnName("is_checked_out")] + public bool? IsCheckedOut { get; set; } + + [ColumnName("borrower")] + public string? Borrower { get; set; } + + [ColumnName("due_date")] + public DateTime? DueDate { get; set; } + + // This column will store vector embeddings. + // The column will use an embedding model from NVIDIA to generate the + // vector embeddings when data is inserted to the column. + [ColumnVectorize( + 1024, + serviceProvider: "nvidia", + serviceModelName: "NV-Embed-QA" + )] + [ColumnName("summary_genres_vector")] + public object? SummaryGenresVector { get; set; } +} + +public class DateTypeTest +{ + [ColumnPrimaryKey()] + public int Id { get; set; } + public DateTime Timestamp { get; set; } + public DateOnly Date { get; set; } + public TimeOnly Time { get; set; } + public DateTime? MaybeTimestamp { get; set; } + public DateOnly? MaybeDate { get; set; } + public TimeOnly? MaybeTime { get; set; } + public DateTime TimestampWithKind { get; set; } +} + +public class UdtTest +{ + [ColumnPrimaryKey()] + public int Id { get; set; } + public TypesTester Udt { get; set; } + public List UdtList { get; set; } +} + +public class UdtTestMinimal +{ + [ColumnPrimaryKey()] + public int Id { get; set; } + public SimpleUdtTwo Udt { get; set; } +} + +[UserDefinedType()] +public class SimpleUdt +{ + public int Number { get; set; } + public string Name { get; set; } +} + +[UserDefinedType()] +public class SimpleUdtTwo +{ + public int Number { get; set; } + public string Name { get; set; } +} + +[UserDefinedType()] +public class TypesTester +{ + public string String { get; set; } + //public System.Net.IPAddress Inet { get; set; } + public int Int { get; set; } + public byte TinyInt { get; set; } + public short SmallInt { get; set; } + public long BigInt { get; set; } + public decimal Decimal { get; set; } + public double Double { get; set; } + public float Float { get; set; } + public bool Boolean { get; set; } + public Guid UUID { get; set; } + public Duration Duration { get; set; } + public DateTime Timestamp { get; set; } + public DateOnly Date { get; set; } + public TimeOnly Time { get; set; } + public DateTime? MaybeTimestamp { get; set; } + public DateOnly? MaybeDate { get; set; } + public TimeOnly? MaybeTime { get; set; } + public DateTime TimestampWithKind { get; set; } } \ No newline at end of file diff --git a/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/AdditionalCollectionTests.cs b/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/AdditionalCollectionTests.cs index f04b687..d4c3cb4 100644 --- a/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/AdditionalCollectionTests.cs +++ b/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/AdditionalCollectionTests.cs @@ -99,5 +99,58 @@ public async Task Book_Projection_Tests() } } + [Fact] + public async Task Test_DateTimeTypes() + { + var collectionName = "collectionTestDateTimeTypes"; + try + { + var collection = await fixture.Database.CreateCollectionAsync(collectionName); + + List documents = new List(); + for (var i = 0; i < 5; i++) + { + documents.Add(new DateTypeTest() + { + Id = i, + Timestamp = DateTime.SpecifyKind(DateTime.Now.AddDays(i), DateTimeKind.Unspecified), + Date = new DateOnly(2000, 1, i + 1), + Time = new TimeOnly(12, i), + TimestampWithKind = DateTime.SpecifyKind(DateTime.Now.AddDays(i), DateTimeKind.Local), + }); + } + for (var i = 5; i < 10; i++) + { + documents.Add(new DateTypeTest() + { + Id = i, + Timestamp = DateTime.SpecifyKind(DateTime.Now.AddDays(i), DateTimeKind.Unspecified), + Date = new DateOnly(2000, 1, i + 1), + Time = new TimeOnly(12, i), + TimestampWithKind = DateTime.SpecifyKind(DateTime.Now.AddDays(i), DateTimeKind.Local), + MaybeDate = new DateOnly(2000, 1, i + 1), + MaybeTime = new TimeOnly(12, i), + MaybeTimestamp = DateTime.SpecifyKind(DateTime.Now.AddDays(i), DateTimeKind.Unspecified), + }); + } + + // Insert the data + var result = await collection.InsertManyAsync(documents); + + Console.WriteLine($"Inserted {result.InsertedCount} rows"); + + Assert.Equal(10, result.InsertedCount); + } + catch (Exception ex) + { + Console.WriteLine(ex); + throw; + } + finally + { + await fixture.Database.DropTableAsync(collectionName); + } + } + } diff --git a/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/AdditionalTableTests.cs b/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/AdditionalTableTests.cs new file mode 100644 index 0000000..c942120 --- /dev/null +++ b/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/AdditionalTableTests.cs @@ -0,0 +1,157 @@ +using DataStax.AstraDB.DataApi.Core; +using DataStax.AstraDB.DataApi.Core.Query; +using DataStax.AstraDB.DataApi.IntegrationTests.Fixtures; +using DataStax.AstraDB.DataApi.Tables; +using Microsoft.VisualBasic; +using System.IO; +using System.Text.Json; +using Xunit; + +namespace DataStax.AstraDB.DataApi.IntegrationTests; + +[Collection("Database")] +public class AdditionalTableTests +{ + DatabaseFixture fixture; + + public AdditionalTableTests(AssemblyFixture assemblyFixture, DatabaseFixture fixture) + { + this.fixture = fixture; + } + + [Fact] + public async Task Test_Arrays() + { + var tableName = "tableFindOneWithArrays"; + try + { + List items = new List() { + new() + { + Id = 0, + StringArray = new string[] { "one", "two", "three" } + }, + new() + { + Id = 1, + StringArray = new string[] { "four", "five", "six" } + }, + new() + { + Id = 2, + StringArray = new string[] { "seven", "eight", "nine" } + }, + }; + + var table = await fixture.Database.CreateTableAsync(tableName); + await table.CreateIndexAsync((b) => b.StringArray); + var insertResult = await table.InsertManyAsync(items); + Assert.Equal(items.Count, insertResult.InsertedIds.Count); + var findOptions = new TableFindOptions() + { + Filter = Builders.Filter.In(x => x.StringArray, new string[] { "five" }), + }; + + var result = await table.FindOneAsync(findOptions); + Assert.NotNull(result); + Assert.Equal(1, result.Id); + } + finally + { + await fixture.Database.DropTableAsync(tableName); + } + } + + [Fact] + public async Task Test_BookJsonData() + { + var tableName = "tableBookWithTestData"; + try + { + var table = await fixture.Database.CreateTableAsync(tableName); + + // Use AppContext.BaseDirectory so the test finds the file when run from the test output folder + var dataFilePath = Path.Combine(AppContext.BaseDirectory ?? Directory.GetCurrentDirectory(), "book_test_data.json"); + // Read the JSON file and parse it into a JSON array + string rawData = await File.ReadAllTextAsync(dataFilePath); + + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower + }; + var rows = JsonSerializer.Deserialize>(rawData, options); + foreach (var row in rows) + { + row.SummaryGenresVector = + $"summary: {row.Summary ?? ""} | genres: {string.Join(", ", row.Genres)}"; + row.DueDate = row.DueDate == null ? null : DateTime.SpecifyKind(row.DueDate.Value, DateTimeKind.Utc); + } + + // Insert the data + var result = await table.InsertManyAsync(rows); + + Console.WriteLine($"Inserted {result.InsertedCount} rows"); + + Assert.Equal(100, result.InsertedCount); + } + finally + { + await fixture.Database.DropTableAsync(tableName); + } + } + + [Fact] + public async Task Test_DateTimeTypes() + { + var tableName = "tableTestDateTimeTypes"; + try + { + var table = await fixture.Database.CreateTableAsync(tableName); + + List rows = new List(); + for (var i = 0; i < 5; i++) + { + rows.Add(new DateTypeTest() + { + Id = i, + Timestamp = DateTime.SpecifyKind(DateTime.Now.AddDays(i), DateTimeKind.Unspecified), + Date = new DateOnly(2000, 1, i + 1), + Time = new TimeOnly(12, i), + TimestampWithKind = DateTime.SpecifyKind(DateTime.Now.AddDays(i), DateTimeKind.Local), + }); + } + for (var i = 5; i < 10; i++) + { + rows.Add(new DateTypeTest() + { + Id = i, + Timestamp = DateTime.SpecifyKind(DateTime.Now.AddDays(i), DateTimeKind.Unspecified), + Date = new DateOnly(2000, 1, i + 1), + Time = new TimeOnly(12, i), + TimestampWithKind = DateTime.SpecifyKind(DateTime.Now.AddDays(i), DateTimeKind.Local), + MaybeDate = new DateOnly(2000, 1, i + 1), + MaybeTime = new TimeOnly(12, i), + MaybeTimestamp = DateTime.SpecifyKind(DateTime.Now.AddDays(i), DateTimeKind.Unspecified), + }); + } + + // Insert the data + var result = await table.InsertManyAsync(rows); + + Console.WriteLine($"Inserted {result.InsertedCount} rows"); + + Assert.Equal(10, result.InsertedCount); + } + catch (Exception ex) + { + Console.WriteLine(ex); + throw; + } + finally + { + await fixture.Database.DropTableAsync(tableName); + } + } + +} + diff --git a/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/DatabaseTests.cs b/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/DatabaseTests.cs index 8010b56..ad2de27 100644 --- a/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/DatabaseTests.cs +++ b/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/DatabaseTests.cs @@ -3,6 +3,7 @@ using DataStax.AstraDB.DataApi.Core.Commands; using DataStax.AstraDB.DataApi.IntegrationTests.Fixtures; using DataStax.AstraDB.DataApi.Tables; +using System.ComponentModel.DataAnnotations; using System.Net; using System.Text; using System.Text.Json; @@ -569,13 +570,14 @@ public async Task CreateTable_DataTypesTest_FromDefinition() try { var createDefinition = new TableDefinition() - .AddTextColumn("Name") - .AddVectorColumn("Vector", 1024) - .AddVectorizeColumn("StringToVectorize", 1024, new VectorServiceOptions + .AddColumn("Name", DataApiType.Text()) + .AddColumn("Vector", DataApiType.Vector(1024)) + .AddColumn("StringToVectorize", DataApiType.Vectorize(1024, new VectorServiceOptions { Provider = "nvidia", ModelName = "NV-Embed-QA" - }) + })) + .AddColumn("Text", DataApiType.Text()) .AddSinglePrimaryKey("Name"); var table = await fixture.Database.CreateTableAsync(tableName, createDefinition); @@ -583,7 +585,7 @@ public async Task CreateTable_DataTypesTest_FromDefinition() var definitions = await fixture.Database.ListTablesAsync(); var definition = definitions.FirstOrDefault(d => d.Name == tableName); Assert.NotNull(definition); - Assert.Equal(1024, (definition.TableDefinition.Columns["Vector"] as VectorColumn).Dimension); + //Assert.Equal(1024, (definition.TableDefinition.Columns["Vector"].).Dimension); } finally { @@ -608,6 +610,7 @@ public async Task CreateTable_CompositePrimaryKey_FromObject() catch (Exception ex) { Console.WriteLine(ex.ToString()); + throw; } finally { @@ -645,11 +648,11 @@ public async Task CreateTable_CompoundPrimaryKey_FromDefinition() try { var createDefinition = new TableDefinition() - .AddTextColumn("KeyOne") - .AddTextColumn("KeyTwo") - .AddTextColumn("Name") - .AddTextColumn("SortOneAscending") - .AddTextColumn("SortTwoDescending") + .AddColumn("KeyOne", DataApiType.Text()) + .AddColumn("KeyTwo", DataApiType.Text()) + .AddColumn("Name", DataApiType.Text()) + .AddColumn("SortOneAscending", DataApiType.Text()) + .AddColumn("SortTwoDescending", DataApiType.Text()) .AddCompoundPrimaryKey("KeyOne", 1) .AddCompoundPrimaryKey("KeyTwo", 2) .AddCompoundPrimaryKeySort("SortOneAscending", 1, SortDirection.Ascending) diff --git a/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/ExamplesTests.cs b/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/ExamplesTests.cs index c7cf215..8e9ebbb 100644 --- a/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/ExamplesTests.cs +++ b/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/ExamplesTests.cs @@ -1,11 +1,12 @@ using DataStax.AstraDB.DataApi.Collections; -using DataStax.AstraDB.DataApi.IntegrationTests.Fixtures; using DataStax.AstraDB.DataApi.Core; +using DataStax.AstraDB.DataApi.IntegrationTests.Fixtures; using Xunit; namespace DataStax.AstraDB.DataApi.IntegrationTests; [Collection("Database")] +[Trait("Category", "Examples")] public class ExamplesTests { DatabaseFixture fixture; diff --git a/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/SearchTests.cs b/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/SearchTests.cs index f8b0d1a..befb807 100644 --- a/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/SearchTests.cs +++ b/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/SearchTests.cs @@ -914,5 +914,48 @@ public void Distinct_WithFilter() var distinct = collection.Find(filter).DistinctBy(so => so.Properties.PropertyOne); Assert.Equal(3, distinct.Count()); } + + [Fact] + public async Task LexicalFindOne() + { + var collectionName = "collectionFindOneWithLexical"; + try + { + List items = new List() { + new() + { + Id = 0, + Name = "This is about a cat.", + }, + new() + { + Id = 1, + Name = "This is about a dog.", + }, + new() + { + Id = 2, + Name = "This is about a horse.", + }, + }; + + var collection = await fixture.Database.CreateCollectionAsync(collectionName); + var insertResult = await collection.InsertManyAsync(items); + Assert.Equal(items.Count, insertResult.InsertedIds.Count); + var findOptions = new DocumentFindOptions() + { + Sort = Builders.Sort.Lexical("dog"), + Filter = Builders.Filter.LexicalMatch("dog"), + }; + + var result = await collection.FindOneAsync(findOptions); + Assert.NotNull(result); + Assert.Equal(1, result.Id); + } + finally + { + await fixture.Database.DropCollectionAsync(collectionName); + } + } } diff --git a/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/TableIndexesTests.cs b/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/TableIndexesTests.cs index 07e7511..5afb477 100644 --- a/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/TableIndexesTests.cs +++ b/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/TableIndexesTests.cs @@ -2,6 +2,7 @@ using DataStax.AstraDB.DataApi.Core.Commands; using DataStax.AstraDB.DataApi.IntegrationTests.Fixtures; using DataStax.AstraDB.DataApi.Tables; +using System.Runtime.CompilerServices; using Xunit; namespace DataStax.AstraDB.DataApi.IntegrationTests; @@ -17,71 +18,141 @@ public TableIndexesTests(AssemblyFixture assemblyFixture, TableIndexesFixture fi } [Fact] - public async Task CreateIndexTests() + public async Task CreateIndexTests_GeneratedIndexNames() { - var table = fixture.Database.GetTable("tableIndexesTest"); - - var indexOptions = new TableIndex + var tableName = "tableIndexesTest"; + try { - IndexName = "category_idx", - Definition = new TableIndexDefinition() + var table = await fixture.Database.CreateTableAsync(tableName); + + // first creation (should succeed) + await table.CreateIndexAsync((b) => b.Category); + + // second creation (should fail) + var ex = await Assert.ThrowsAsync(() => + table.CreateIndexAsync((b) => b.Category)); + + Assert.Contains("already exists", ex.Message); + + // second creation (should not fail when SkipIfExists is set) + await table.CreateIndexAsync((b) => b.Category, new CreateIndexCommandOptions() { - Column = (b) => b.Category, - Normalize = true, - CaseSensitive = false - } - }; + SkipIfExists = true + }); - // first creation (should succeed) - await table.CreateIndexAsync(indexOptions); + var result = await table.ListIndexMetadataAsync(); + Assert.Contains(result.Indexes, i => i.Name == "category_idx"); - // second creation (should fail) - var ex = await Assert.ThrowsAsync(() => - table.CreateIndexAsync(indexOptions)); + //ensure insert still works + var insertResult = await TableIndexesFixture.AddTableRows(table); + Assert.Equal(3, insertResult.InsertedCount); - Assert.Contains("already exists", ex.Message); + } + finally + { + await fixture.Database.DropTableAsync(tableName); + } + } - // second creation (should not fail when SkipIfExists is set) - await table.CreateIndexAsync(indexOptions, new CreateIndexCommandOptions() + [Fact] + public async Task CreateIndexTests_NamedIndex() + { + var tableName = "tableIndexesTest_NamedIndex"; + try { - SkipIfExists = true - }); + var table = await fixture.Database.CreateTableAsync(tableName); - var result = await table.ListIndexMetadataAsync(); - Assert.Contains(result.Indexes, i => i.Name == "category_idx"); + // first creation (should succeed) + await table.CreateIndexAsync("category_idx", (b) => b.Category); + + // second creation (should fail) + var ex = await Assert.ThrowsAsync(() => + table.CreateIndexAsync("category_idx", (b) => b.Category)); + + Assert.Contains("already exists", ex.Message); + + // second creation (should not fail when SkipIfExists is set) + await table.CreateIndexAsync("category_idx", (b) => b.Category, new CreateIndexCommandOptions() + { + SkipIfExists = true + }); + + var result = await table.ListIndexMetadataAsync(); + Assert.Contains(result.Indexes, i => i.Name == "category_idx"); + + var insertResult = await TableIndexesFixture.AddTableRows(table); + Assert.Equal(3, insertResult.InsertedCount); + } + finally + { + await fixture.Database.DropTableAsync(tableName); + } } [Fact] - public async Task DropIndexTests() + public async Task DropIndexTests_NamedIndex() { - var table = fixture.Database.GetTable("tableIndexesTest"); - - var indexName = "drop_idx"; - var indexOptions = new TableIndex + var tableName = "dropIndexTest"; + try { - IndexName = indexName, - Definition = new TableIndexDefinition() + var table = await fixture.Database.CreateTableAsync(tableName); + + var indexName = "drop_idx"; + + await table.CreateIndexAsync(indexName, (b) => b.Location); + + // drop should work + await fixture.Database.DropTableIndexAsync(indexName); + var result = await table.ListIndexMetadataAsync(); + Assert.DoesNotContain(result.Indexes, i => i.Name == indexName); + + // second drop (should fail) + var ex = await Assert.ThrowsAsync(() => + fixture.Database.DropTableIndexAsync(indexName)); + + // second drop (should not fail when SkipIfExists is set) + await fixture.Database.DropTableIndexAsync(indexName, new DropIndexCommandOptions() { - Column = (b) => b.Location - } - }; - await table.CreateIndexAsync(indexOptions); - - // drop should work - await fixture.Database.DropTableIndexAsync(indexName); - var result = await table.ListIndexMetadataAsync(); - Assert.DoesNotContain(result.Indexes, i => i.Name == indexName); - - // second drop (should fail) - var ex = await Assert.ThrowsAsync(() => - fixture.Database.DropTableIndexAsync(indexName)); - - // second drop (should not fail when SkipIfExists is set) - await fixture.Database.DropTableIndexAsync(indexName, new DropIndexCommandOptions() + SkipIfNotExists = true + }); + } + finally + { + await fixture.Database.DropTableAsync(tableName); + } + + } + + [Fact] + public async Task DropIndexTests_GeneratedIndexNames() + { + var tableName = "dropIndexTests_GeneratedIndexNames"; + try { - SkipIfNotExists = true - }); + var table = await fixture.Database.CreateTableAsync(tableName); + + var indexName = "Location_idx"; + await table.CreateIndexAsync(indexName, (b) => b.Location); + // drop should work + await fixture.Database.DropTableIndexAsync(indexName); + var result = await table.ListIndexMetadataAsync(); + Assert.DoesNotContain(result.Indexes, i => i.Name == indexName); + + // second drop (should fail) + var ex = await Assert.ThrowsAsync(() => + fixture.Database.DropTableIndexAsync(indexName)); + + // second drop (should not fail when SkipIfExists is set) + await fixture.Database.DropTableIndexAsync(indexName, new DropIndexCommandOptions() + { + SkipIfNotExists = true + }); + } + finally + { + await fixture.Database.DropTableAsync(tableName); + } } diff --git a/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/TableTests.cs b/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/TableTests.cs index 7c86545..e1b661c 100644 --- a/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/TableTests.cs +++ b/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/TableTests.cs @@ -3,6 +3,8 @@ using DataStax.AstraDB.DataApi.IntegrationTests.Fixtures; using DataStax.AstraDB.DataApi.Tables; using Microsoft.VisualBasic; +using System.Net; +using System.Text; using Xunit; namespace DataStax.AstraDB.DataApi.IntegrationTests; @@ -266,14 +268,7 @@ public async Task Delete_One() rows.Add(row); } var table = await fixture.Database.CreateTableAsync(tableName); - await table.CreateIndexAsync(new TableIndex() - { - IndexName = "testDeleteOne_number_of_pages_index", - Definition = new TableIndexDefinition() - { - Column = (b) => b.NumberOfPages - } - }); + await table.CreateIndexAsync("testDeleteOne_number_of_pages_index", (b) => b.NumberOfPages); await table.InsertManyAsync(rows); var filter = Builders.Filter .Eq(so => so.Title, "Title 1"); @@ -327,14 +322,7 @@ public async Task Delete_Many() rows.Add(row); } var table = await fixture.Database.CreateTableAsync(tableName); - await table.CreateIndexAsync(new TableIndex() - { - IndexName = "testDeleteOne_number_of_pages_index", - Definition = new TableIndexDefinition() - { - Column = (b) => b.NumberOfPages - } - }); + await table.CreateIndexAsync("testDeleteOne_number_of_pages_index", (b) => b.NumberOfPages); await table.InsertManyAsync(rows); var filter = Builders.Filter .Eq(so => so.Title, "Title 1"); @@ -382,6 +370,7 @@ public async Task Delete_CompositePrimaryKey() catch (Exception ex) { var msg = ex.Message; + throw; } finally { @@ -607,5 +596,54 @@ public async Task Update_Test_Untyped() Assert.Equal("Name_3_Updated", updatedDocument["Name"].ToString()); } + [Fact] + public async Task FindOne_Lexical() + { + var tableName = "tableFindOneWithLexical"; + try + { + List items = new List() { + new() + { + Id = 0, + Name = "This is about a cat.", + }, + new() + { + Id = 1, + Name = "This is about a dog.", + }, + new() + { + Id = 2, + Name = "This is about a horse.", + }, + }; + + var table = await fixture.Database.CreateTableAsync(tableName); + await table.CreateIndexAsync((b) => b.LexicalValue, Builders.TableIndex.Text()); + var insertResult = await table.InsertManyAsync(items); + Assert.Equal(items.Count, insertResult.InsertedIds.Count); + var findOptions = new TableFindOptions() + { + Sort = Builders.TableSort.Lexical((b) => b.LexicalValue, "dog"), + Filter = Builders.Filter.LexicalMatch("dog"), + }; + + var result = await table.FindOneAsync(findOptions); + Assert.NotNull(result); + Assert.Equal(1, result.Id); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + throw; + } + finally + { + await fixture.Database.DropTableAsync(tableName); + } + } + } diff --git a/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/UserDefinedTypesTest.cs b/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/UserDefinedTypesTest.cs new file mode 100644 index 0000000..985f63a --- /dev/null +++ b/test/DataStax.AstraDB.DataApi.IntegrationTests/Tests/UserDefinedTypesTest.cs @@ -0,0 +1,275 @@ +using DataStax.AstraDB.DataApi.Core; +using DataStax.AstraDB.DataApi.Core.Query; +using DataStax.AstraDB.DataApi.IntegrationTests.Fixtures; +using DataStax.AstraDB.DataApi.Tables; +using Microsoft.VisualBasic; +using Newtonsoft.Json; +using System.Net; +using System.Text; +using Xunit; + +namespace DataStax.AstraDB.DataApi.IntegrationTests; + +[Collection("UserDefinedTypes")] +public class UserDefinedTypesTests +{ + UserDefinedTypesFixture fixture; + + public UserDefinedTypesTests(AssemblyFixture assemblyFixture, UserDefinedTypesFixture fixture) + { + this.fixture = fixture; + } + + [Fact] + public async Task UserDefinedTypes_Test() + { + const string typeName = "testUserDefinedType"; + try + { + await fixture.Database.CreateTypeAsync(typeName, new UserDefinedTypeDefinition + { + Fields = new Dictionary + { + ["id"] = DataApiType.Int(), + ["name"] = DataApiType.Text(), + ["removeMe"] = DataApiType.Boolean() + } + }); + + var types = await fixture.Database.ListTypesAsync(); + Assert.Contains(types, t => t.Name.Equals(typeName, StringComparison.OrdinalIgnoreCase)); + Assert.True(types.First(t => t.Name.Equals(typeName, StringComparison.OrdinalIgnoreCase)) + .Definition.Fields.ContainsKey("removeMe")); + + var alterDefinition = new AlterUserDefinedTypeDefinition(typeName) + .AddField("new_field", DataApiType.Text()) + .RenameField("removeMe", "okYouCanStay"); + + await fixture.Database.AlterTypeAsync(alterDefinition); + var updatedTypes = await fixture.Database.ListTypesAsync(); + var updatedType = updatedTypes.FirstOrDefault(t => t.Name.Equals(typeName, StringComparison.OrdinalIgnoreCase)); + Assert.NotNull(updatedType); + Assert.True(updatedType.Definition.Fields.ContainsKey("new_field")); + Assert.True(updatedType.Definition.Fields.ContainsKey("okYouCanStay")); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + throw; + } + finally + { + await fixture.Database.DropTypeAsync(typeName); + } + } + + + [Fact] + public async Task UserDefinedTypes_CreateFromClasses_Basic() + { + var tableName = "userDefinedTypesFromClassesBasic"; + try + { + List items = new List() { + new() + { + Id = 0, + Udt = new SimpleUdtTwo + { + Name = "Test 1", + Number = 101 + }, + }, + new() + { + Id = 1, + Udt = new SimpleUdtTwo + { + Name = "Test 2", + Number = 102 + }, + }, + new() + { + Id = 2, + Udt = new SimpleUdtTwo + { + Name = "Test 3", + Number = 103 + }, + }, + }; + + var table = await fixture.Database.CreateTableAsync(tableName); + var insertResult = await table.InsertManyAsync(items); + Assert.Equal(items.Count, insertResult.InsertedIds.Count); + var filter = Builders.Filter.Eq(b => b.Udt.Name, "Test 3"); + + //TODO: Can you filter on UDT fields? + // var result = await table.FindOneAsync(filter); + // Assert.NotNull(result); + // Assert.Equal(2, result.Id); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + throw; + } + finally + { + await fixture.Database.DropTableAsync(tableName); + await fixture.Database.DropTypeAsync(); + } + } + + [Fact] + public async Task UserDefinedTypes_CreateFromClasses() + { + var tableName = "userDefinedTypesFromClasses"; + try + { + List items = new List() { + new() + { + Id = 0, + Udt = new TypesTester + { + String = "Test 1", + //Inet = IPAddress.Parse("192.168.0.1"), + Int = int.MaxValue, + TinyInt = byte.MaxValue, + SmallInt = short.MaxValue, + BigInt = long.MaxValue, + Decimal = decimal.MaxValue, + Double = double.MaxValue, + Float = float.MaxValue, + Boolean = false, + UUID = Guid.NewGuid(), + Duration = Duration.Parse("12y3mo1d12h30m5s12ms7us1ns"), + Timestamp = DateTime.Now, + Date = DateOnly.FromDateTime(DateTime.Now), + Time = TimeOnly.FromDateTime(DateTime.Now), + MaybeTimestamp = null, + MaybeDate = DateOnly.FromDateTime(DateTime.Now), + MaybeTime = null, + TimestampWithKind = DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Utc), + }, + UdtList = new List + { + new SimpleUdt + { + Name = "List Test 1", + Number = 1001 + }, + new SimpleUdt + { + Name = "List Test 2", + Number = 1002 + } + }, + }, + new() + { + Id = 1, + Udt = new TypesTester + { + String = "Test 2", + //Inet = IPAddress.Parse("192.168.0.2"), + Int = int.MaxValue, + TinyInt = byte.MaxValue, + SmallInt = short.MaxValue, + BigInt = long.MaxValue, + Decimal = decimal.MaxValue, + Double = double.MaxValue, + Float = float.MaxValue, + Boolean = false, + UUID = Guid.NewGuid(), + Duration = Duration.Parse("12y3mo1d12h30m5s12ms7us2ns"), + Timestamp = DateTime.Now, + Date = DateOnly.FromDateTime(DateTime.Now), + Time = TimeOnly.FromDateTime(DateTime.Now), + MaybeTimestamp = null, + MaybeDate = DateOnly.FromDateTime(DateTime.Now), + MaybeTime = null, + TimestampWithKind = DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Utc), + }, + UdtList = new List + { + new SimpleUdt + { + Name = "List Test 2 dot 1", + Number = 2001 + }, + new SimpleUdt + { + Name = "List Test 2 dot 2", + Number = 2002 + } + }, + }, + new() + { + Id = 2, + Udt = new TypesTester + { + String = "Test 3", + //Inet = IPAddress.Parse("192.168.0.3"), + Int = int.MaxValue, + TinyInt = byte.MaxValue, + SmallInt = short.MaxValue, + BigInt = long.MaxValue, + Decimal = decimal.MaxValue, + Double = double.MaxValue, + Float = float.MaxValue, + Boolean = false, + UUID = Guid.NewGuid(), + Duration = Duration.Parse("12y3mo1d12h30m5s12ms7us2ns"), + Timestamp = DateTime.Now, + Date = DateOnly.FromDateTime(DateTime.Now), + Time = TimeOnly.FromDateTime(DateTime.Now), + MaybeTimestamp = null, + MaybeDate = DateOnly.FromDateTime(DateTime.Now), + MaybeTime = null, + TimestampWithKind = DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Utc), + }, + UdtList = new List + { + new SimpleUdt + { + Name = "List Test 3 dot 1", + Number = 3001 + }, + new SimpleUdt + { + Name = "List Test 3 dot 2", + Number = 3002 + } + }, + }, + }; + + var table = await fixture.Database.CreateTableAsync(tableName); + var insertResult = await table.InsertManyAsync(items); + Assert.Equal(items.Count, insertResult.InsertedIds.Count); + var filter = Builders.Filter.Eq(b => b.Udt.String, "Test 3"); + + //TODO: Can you filter on UDT fields? + // var result = await table.FindOneAsync(filter); + // Assert.NotNull(result); + // Assert.Equal(2, result.Id); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + throw; + } + finally + { + await fixture.Database.DropTableAsync(tableName); + await fixture.Database.DropTypeAsync(); + await fixture.Database.DropTypeAsync(); + } + } + +} + diff --git a/test/DataStax.AstraDB.DataApi.IntegrationTests/book_test_data.json b/test/DataStax.AstraDB.DataApi.IntegrationTests/book_test_data.json new file mode 100644 index 0000000..ff19f5f --- /dev/null +++ b/test/DataStax.AstraDB.DataApi.IntegrationTests/book_test_data.json @@ -0,0 +1,2055 @@ +[ + { + "title": "Hidden Shadows of the Past", + "author": "John Anthony", + "number_of_pages": 481, + "rating": 1.0, + "publication_year": 2002, + "summary": "Set against a forgotten metropolis, 'Hidden Shadows of the Past' by John Anthony unveils a journey to the underworld. The story builds through defying a cruel god, offering a gripping tale of suspense.", + "genres": [ + "Biography", + "Graphic Novel", + "Dystopian", + "Drama" + ], + "metadata": { + "isbn": "978-1-905585-40-3", + "language": "French", + "edition": "Anniversary Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Beyond Moments of Broken Promises", + "author": "Daniel Larson", + "number_of_pages": 704, + "rating": 3.7, + "publication_year": 1978, + "summary": "Set against a secret base in the Arctic, 'Beyond Moments of Broken Promises' by Daniel Larson unveils a search for inner peace. The story builds through rediscovering a lost friendship, offering a thrilling and action-packed blockbuster.", + "genres": [ + "Dystopian" + ], + "metadata": { + "isbn": "978-0-88734-027-7", + "language": "Japanese", + "edition": "Anniversary Edition" + }, + "is_checked_out": true, + "borrower": "Alexander Rodriguez", + "due_date": "2024-12-18" + }, + { + "title": "Within Echoes Through Time", + "author": "Ryan Jackson", + "number_of_pages": 714, + "rating": 2.4, + "publication_year": 1993, + "summary": "'Within Echoes Through Time' by Ryan Jackson immerses readers in a magical forest, where a quest for the truth collides with navigating treacherous terrain, delivering a rich and immersive world.", + "genres": [ + "Tragedy", + "Fantasy", + "Satire" + ], + "metadata": { + "isbn": "978-0-02-313022-9", + "language": "Spanish", + "edition": "Illustrated Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "The Flames of Eternal Night", + "author": "Deanna Fisher", + "number_of_pages": 198, + "rating": 4.6, + "publication_year": 1996, + "summary": "Deanna Fisher's 'The Flames of Eternal Night' is a journey to the edge of the world that takes place in a forgotten metropolis. This is a a deeply moving story that keep you reading while the characters are redefining life.", + "genres": [ + "Dystopian", + "Adventure" + ], + "metadata": { + "isbn": "978-1-5358-6857-0", + "language": "German", + "edition": "Illustrated Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Against Legends of Brightness", + "author": "Christopher Washington", + "number_of_pages": 327, + "rating": 1.1, + "publication_year": 1975, + "summary": "'Against Legends of Brightness' is Christopher Washington's brilliant novel, set in a hidden library. Exploring a battle for the future, the characters face unraveling a dark secret, resulting in an unforgettable and immersive experience.", + "genres": [ + "Memoir", + "History" + ], + "metadata": { + "isbn": "978-0-11-359399-6", + "language": "Spanish", + "edition": "Third" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Beyond Waves that Changed Everything", + "author": "Jeffrey Davis", + "number_of_pages": 185, + "rating": 4.3, + "publication_year": 2020, + "summary": "In the backdrop of a futuristic laboratory, 'Beyond Waves that Changed Everything' by Jeffrey Davis explores a journey of self-discovery. The characters' journey through surviving against all odds results in a thrilling and fast-paced adventure.", + "genres": [ + "Biography", + "Epic" + ], + "metadata": { + "isbn": "978-0-309-98069-2", + "language": "Italian", + "edition": "Anniversary Edition" + }, + "is_checked_out": true, + "borrower": "James Hanson", + "due_date": "2024-11-28" + }, + { + "title": "Into Shadows of Tomorrow", + "author": "Nicole Wright", + "number_of_pages": 598, + "rating": 3.1, + "publication_year": 2020, + "summary": "Set against a distant alien world, 'Into Shadows of Tomorrow' by Nicole Wright unveils an unraveling conspiracy. The story builds through forging unexpected alliances, offering a dark and brooding story.", + "genres": [ + "Thriller", + "Philosophy", + "Fantasy" + ], + "metadata": { + "isbn": "978-1-4715-0356-6", + "language": "Italian", + "edition": "Anniversary Edition" + }, + "is_checked_out": true, + "borrower": "Gregory Adams", + "due_date": "2024-12-05" + }, + { + "title": "Beyond Tides of a Lifetime", + "author": "Kendra Knight", + "number_of_pages": 356, + "rating": 1.9, + "publication_year": 1987, + "summary": "'Beyond Tides of a Lifetime' is Kendra Knight's latest masterpiece set in a distant alien world. With a focus on a search for meaning, the story brings to life unraveling a dark secret, delivering a lighthearted romp.", + "genres": [ + "Thriller", + "History", + "Epic", + "Philosophy" + ], + "metadata": { + "isbn": "978-0-295-38479-5", + "language": "Italian", + "edition": "Second" + }, + "is_checked_out": true, + "borrower": "Leslie Hansen", + "due_date": "2024-12-05" + }, + { + "title": "The Shadows of Eternal Night", + "author": "Martin Harris", + "number_of_pages": 198, + "rating": 1.0, + "publication_year": 2007, + "summary": "Martin Harris's masterpiece, 'The Shadows of Eternal Night', is a story set in a crumbling mansion. Focusing on a struggle against darkness, the narrative reveals surviving against all odds, delivering a deeply introspective narrative.", + "genres": [ + "Thriller" + ], + "metadata": { + "isbn": "978-1-301-16043-3", + "language": "Chinese", + "edition": "Special Release" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Within the Horizon and the Last Frontier", + "author": "Frank Costa", + "number_of_pages": 385, + "rating": 4.0, + "publication_year": 2018, + "summary": "In a mysterious castle, 'Within the Horizon and the Last Frontier' by Frank Costa takes readers on a journey through a search for lost treasure. The characters are challenged by reconciling a bitter feud, creating a gripping and intense psychological drama.", + "genres": [ + "Dystopian", + "Adventure", + "Science Fiction", + "Non-Fiction" + ], + "metadata": { + "isbn": "978-1-138-81334-2", + "language": "Japanese", + "edition": "Anniversary Edition" + }, + "is_checked_out": true, + "borrower": "Cody Harris", + "due_date": "2024-12-18" + }, + { + "title": "The Shadows of Brightness", + "author": "Sara Jones", + "number_of_pages": 185, + "rating": 2.9, + "publication_year": 1995, + "summary": "Within the world of a crumbling mansion, Sara Jones's 'The Shadows of Brightness' unravels a tale of a journey to the afterlife. Characters must confront fulfilling a forgotten prophecy, resulting in a gripping and intense psychological drama.", + "genres": [ + "Philosophy", + "Memoir", + "Romance" + ], + "metadata": { + "isbn": "978-1-57611-473-5", + "language": "English", + "edition": "Second" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Under Secrets and the Unknown", + "author": "Ashley Villanueva", + "number_of_pages": 225, + "rating": 3.4, + "publication_year": 1992, + "summary": "Within a war-torn kingdom, 'Under Secrets and the Unknown' by Ashley Villanueva captures the spirit of a search for meaning. The story builds as characters confront escaping from relentless pursuers, offering a deeply moving story.", + "genres": [ + "Fantasy", + "Romance", + "Adventure" + ], + "metadata": { + "isbn": "978-0-261-26454-0", + "language": "Korean", + "edition": "Anniversary Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Under Waves Through Time", + "author": "Nicholas Fischer", + "number_of_pages": 162, + "rating": 4.0, + "publication_year": 1998, + "summary": "Nicholas Fischer's masterpiece, 'Under Waves Through Time', is a story set in a war-torn kingdom. Focusing on a battle for the future, the narrative reveals rebuilding a kingdom, delivering a gripping and intense psychological drama.", + "genres": [ + "Graphic Novel" + ], + "metadata": { + "isbn": "978-1-74188-720-4", + "language": "Italian", + "edition": "Special Release" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Beneath Notes of the Past", + "author": "Mary Robinson", + "number_of_pages": 643, + "rating": 1.4, + "publication_year": 1980, + "summary": "Set amidst a distant alien world, 'Beneath Notes of the Past' by Mary Robinson takes on a battle against the elements, where facing their greatest fears leads to a whimsical and enchanting fable.", + "genres": [ + "Satire" + ], + "metadata": { + "isbn": "978-1-65916-871-6", + "language": "Chinese", + "edition": "Second" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Beneath Whispers that Changed Everything", + "author": "Sarah Lewis", + "number_of_pages": 690, + "rating": 4.1, + "publication_year": 2024, + "summary": "In the backdrop of a futuristic laboratory, 'Beneath Whispers that Changed Everything' by Sarah Lewis explores a struggle for power. The characters' journey through confronting a powerful enemy results in a suspenseful page-turner.", + "genres": [ + "Horror" + ], + "metadata": { + "isbn": "978-1-76888-482-8", + "language": "Italian", + "edition": "Illustrated Edition" + }, + "is_checked_out": true, + "borrower": "Samantha Davis", + "due_date": "2024-12-02" + }, + { + "title": "In Secrets in the Dark", + "author": "Anne Patrick", + "number_of_pages": 521, + "rating": 4.2, + "publication_year": 2022, + "summary": "In 'In Secrets in the Dark', Anne Patrick weaves a tale set in an isolated lighthouse, centered around a fight for survival. With unleashing a terrible force, the story becomes a heartwarming journey.", + "genres": [ + "Non-Fiction" + ], + "metadata": { + "isbn": "978-0-218-35196-5", + "language": "French", + "edition": "Anniversary Edition" + }, + "is_checked_out": true, + "borrower": "Charles Leach", + "due_date": "2024-11-26" + }, + { + "title": "Among Storms on the Edge", + "author": "Corey Hernandez", + "number_of_pages": 392, + "rating": 4.3, + "publication_year": 1998, + "summary": "Set in an abandoned carnival, 'Among Storms on the Edge' by Corey Hernandez dives into a quest for peace. Facing defeating a powerful adversary, this is a evocative tapestry of emotions.", + "genres": [ + "Adventure", + "Philosophy" + ], + "metadata": { + "isbn": "978-1-65246-664-2", + "language": "English", + "edition": "Limited Edition" + }, + "is_checked_out": true, + "borrower": "Ryan Reyes", + "due_date": "2024-12-09" + }, + { + "title": "Under Ghosts in the Universe", + "author": "Tanya Perez", + "number_of_pages": 345, + "rating": 4.6, + "publication_year": 2001, + "summary": "'Under Ghosts in the Universe', a novel by Tanya Perez, is set in an underwater cave, where a battle for the ages unfolds. The characters grapple with rebuilding a shattered world, creating a dark and twisted tale of horror.", + "genres": [ + "Dystopian", + "Drama", + "Poetry", + "Comedy" + ], + "metadata": { + "isbn": "978-0-489-60732-7", + "language": "Arabic", + "edition": "Collector's Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "From Operas and the Last Frontier", + "author": "Yolanda Anderson", + "number_of_pages": 412, + "rating": 1.7, + "publication_year": 2017, + "summary": "Yolanda Anderson's 'From Operas and the Last Frontier' is a quest for vengeance that takes place in a haunted shack. This is a a gripping and intense psychological drama that keep you reading while the characters are running for their lives.", + "genres": [ + "Drama", + "Adventure", + "Self-Help", + "Satire" + ], + "metadata": { + "isbn": "978-0-8461-2621-8", + "language": "Spanish", + "edition": "Deluxe Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Within the Horizon of the Heart", + "author": "Randy Cook", + "number_of_pages": 171, + "rating": 4.2, + "publication_year": 2015, + "summary": "In an abandoned carnival, 'Within the Horizon of the Heart' by Randy Cook takes readers on a journey through a battle for the future. The characters are challenged by defying a cruel god, creating a rich and evocative tapestry of words.", + "genres": [ + "Poetry", + "Biography" + ], + "metadata": { + "isbn": "978-0-7369-1813-8", + "language": "Russian", + "edition": "Deluxe Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "The Flames in the Dark", + "author": "Jason Choi", + "number_of_pages": 595, + "rating": 3.3, + "publication_year": 2009, + "summary": "'The Flames in the Dark' is Jason Choi's brilliant novel, set in a secret base in the Arctic. Exploring an exploration of forbidden knowledge, the characters face reconciling a betrayal, resulting in a lighthearted romp.", + "genres": [ + "History", + "Satire", + "Drama" + ], + "metadata": { + "isbn": "978-0-341-02285-5", + "language": "Spanish", + "edition": "First" + }, + "is_checked_out": true, + "borrower": "Kelly Green", + "due_date": "2024-12-04" + }, + { + "title": "Into Acquaintances Through Time", + "author": "Cynthia Duncan", + "number_of_pages": 246, + "rating": 3.9, + "publication_year": 2006, + "summary": "Set in a haunted shack, 'Into Acquaintances Through Time' by Cynthia Duncan dives into a search for purpose. Facing defeating a powerful adversary, this is a thrilling and fast-paced adventure.", + "genres": [ + "Science Fiction" + ], + "metadata": { + "isbn": "978-1-190-67288-8", + "language": "German", + "edition": "Deluxe Edition" + }, + "is_checked_out": true, + "borrower": "Charles Rubio", + "due_date": "2024-12-21" + }, + { + "title": "Hidden Moons on the Edge", + "author": "Kristin Bell", + "number_of_pages": 725, + "rating": 1.8, + "publication_year": 1975, + "summary": "In the backdrop of an abandoned carnival, 'Hidden Moons on the Edge' by Kristin Bell explores a quest for the truth. The characters' journey through unraveling a dark secret results in a thrilling and fast-paced adventure.", + "genres": [ + "Graphic Novel" + ], + "metadata": { + "isbn": "978-1-63808-366-5", + "language": "Chinese", + "edition": "Anniversary Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Beneath Voices of Yesterday", + "author": "Rodney Hopkins", + "number_of_pages": 638, + "rating": 1.3, + "publication_year": 1985, + "summary": "'Beneath Voices of Yesterday' by Rodney Hopkins is set in an oasis of art and tells a story of a journey to the stars. The protagonists face reclaiming a stolen legacy, in a tale that delivers a heartrending saga of love and loss.", + "genres": [ + "Romance", + "Thriller", + "Mystery", + "Young Adult" + ], + "metadata": { + "isbn": "978-1-65322-291-9", + "language": "French", + "edition": "Third" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "A Phantoms of the Heart", + "author": "Steven Jackson", + "number_of_pages": 723, + "rating": 3.1, + "publication_year": 1989, + "summary": "Set in a remote island, 'A Phantoms of the Heart' by Steven Jackson dives into an unraveling conspiracy. Facing rekindling a lost love, this is a deep character study.", + "genres": [ + "Non-Fiction" + ], + "metadata": { + "isbn": "978-1-883578-61-9", + "language": "Korean", + "edition": "Deluxe Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Beneath Waves of Yesterday", + "author": "Tonya Kennedy", + "number_of_pages": 153, + "rating": 2.7, + "publication_year": 1982, + "summary": "In a hidden library, Tonya Kennedy's 'Beneath Waves of Yesterday' unravels a gripping tale of a struggle for power. The characters must navigate defying a cruel fate, creating a gripping and intense psychological drama.", + "genres": [ + "Comedy" + ], + "metadata": { + "isbn": "978-0-19-555638-4", + "language": "Japanese", + "edition": "First" + }, + "is_checked_out": true, + "borrower": "Carol Edwards", + "due_date": "2024-12-03" + }, + { + "title": "Among Ghosts in the Dark", + "author": "Michael Rojas", + "number_of_pages": 199, + "rating": 2.8, + "publication_year": 2013, + "summary": "Set in a war-torn kingdom, 'Among Ghosts in the Dark' by Michael Rojas dives into an exploration of forbidden knowledge. Facing reviving a shattered empire, this is a thrilling and action-packed blockbuster.", + "genres": [ + "Graphic Novel", + "Mystery", + "Adventure", + "Dystopian" + ], + "metadata": { + "isbn": "978-1-255-54864-6", + "language": "French", + "edition": "First" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Within Light on the Edge", + "author": "Kathy Vincent", + "number_of_pages": 166, + "rating": 3.1, + "publication_year": 1985, + "summary": "Set against a hidden library, 'Within Light on the Edge' by Kathy Vincent unveils a search for lost treasure. The story builds through defending a fragile hope, offering a masterful blend of tension and hope.", + "genres": [ + "Self-Help" + ], + "metadata": { + "isbn": "978-0-9823322-8-3", + "language": "Japanese", + "edition": "Deluxe Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Under Storms of Lost Souls", + "author": "Tammy Ramos", + "number_of_pages": 696, + "rating": 5.0, + "publication_year": 1988, + "summary": "Within a sprawling museum, 'Under Storms of Lost Souls' by Tammy Ramos captures the spirit of a quest for the truth. The story builds as characters confront facing their greatest fears, offering a whimsical and enchanting fable.", + "genres": [ + "Poetry", + "Historical Fiction", + "Graphic Novel", + "Satire" + ], + "metadata": { + "isbn": "978-1-907161-02-5", + "language": "Japanese", + "edition": "Collector's Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Against Light of Eternal Night", + "author": "Jennifer Ray", + "number_of_pages": 655, + "rating": 4.8, + "publication_year": 1999, + "summary": "'Against Light of Eternal Night', a novel by Jennifer Ray, is set in a mysterious castle, where a fight for love unfolds. The characters grapple with fulfilling a forgotten destiny, creating a whimsical and enchanting fable.", + "genres": [ + "Romance" + ], + "metadata": { + "isbn": "978-0-213-40965-4", + "language": "Portuguese", + "edition": "Collector's Edition" + }, + "is_checked_out": true, + "borrower": "Benjamin Franklin", + "due_date": "2024-12-12" + }, + { + "title": "Under Operas of Tomorrow", + "author": "Brittany Park", + "number_of_pages": 435, + "rating": 2.8, + "publication_year": 1988, + "summary": "'Under Operas of Tomorrow', written by Brittany Park, is a story set in a mysterious castle. Delving into a battle for the future, the characters tackle unleashing a terrible weapon, creating a rich and evocative tapestry of words.", + "genres": [ + "Romance", + "Science Fiction", + "Fantasy", + "Self-Help" + ], + "metadata": { + "isbn": "978-0-524-06824-3", + "language": "Korean", + "edition": "Deluxe Edition" + }, + "is_checked_out": true, + "borrower": "Elizabeth Atkinson", + "due_date": "2024-12-15" + }, + { + "title": "A Operas of a Lifetime", + "author": "Jonathan Solis", + "number_of_pages": 703, + "rating": 3.1, + "publication_year": 2021, + "summary": "In Jonathan Solis's 'A Operas of a Lifetime', set in a mysterious castle, readers explore a quest for immortality. Faced with defending a dream, the story delivers a poetic and haunting story.", + "genres": [ + "Dystopian", + "History" + ], + "metadata": { + "isbn": "978-1-124-33296-3", + "language": "Italian", + "edition": "Second" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Under Echoes of the Heart", + "author": "Kevin White", + "number_of_pages": 546, + "rating": 2.9, + "publication_year": 1975, + "summary": "In an abandoned carnival, 'Under Echoes of the Heart' by Kevin White takes readers on a journey through an unraveling conspiracy. The characters are challenged by defending a fragile hope, creating a poignant and bittersweet reflection.", + "genres": [ + "Historical Fiction", + "Drama", + "Memoir", + "History" + ], + "metadata": { + "isbn": "978-1-75598-068-7", + "language": "Italian", + "edition": "Deluxe Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Through Darkness of Brightness", + "author": "Darrell Vargas", + "number_of_pages": 740, + "rating": 4.6, + "publication_year": 1997, + "summary": "Set amidst a crumbling mansion, 'Through Darkness of Brightness' by Darrell Vargas takes on a struggle against destiny, where finding a way home leads to a gripping tale of suspense.", + "genres": [ + "Epic", + "Biography", + "Thriller", + "Memoir" + ], + "metadata": { + "isbn": "978-0-06-985516-2", + "language": "Chinese", + "edition": "Anniversary Edition" + }, + "is_checked_out": true, + "borrower": "Brian Lee", + "due_date": "2024-12-10" + }, + { + "title": "Under Moments Through Time", + "author": "Lawrence Kirby", + "number_of_pages": 337, + "rating": 1.6, + "publication_year": 1999, + "summary": "In Lawrence Kirby's 'Under Moments Through Time', set in an underwater cave, readers explore a forbidden love affair. Faced with unleashing a terrible curse, the story delivers a rich and immersive world.", + "genres": [ + "Historical Fiction", + "Self-Help" + ], + "metadata": { + "isbn": "978-0-9879277-1-2", + "language": "German", + "edition": "First" + }, + "is_checked_out": true, + "borrower": "Penny Hayes", + "due_date": "2024-12-19" + }, + { + "title": "Under Friendships and the Journey", + "author": "Edward Rojas", + "number_of_pages": 344, + "rating": 1.5, + "publication_year": 1991, + "summary": "Set against an isolated lighthouse, 'Under Friendships and the Journey' by Edward Rojas unveils a battle for freedom. The story builds through unlocking a hidden power, offering a dark and twisted tale of horror.", + "genres": [ + "Romance" + ], + "metadata": { + "isbn": "978-1-84463-203-9", + "language": "Korean", + "edition": "Collector's Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Within Silence and the Journey", + "author": "Ashley Oliver", + "number_of_pages": 420, + "rating": 2.5, + "publication_year": 2022, + "summary": "Within the world of a mysterious castle, Ashley Oliver's 'Within Silence and the Journey' unravels a tale of a struggle for power. Characters must confront facing their greatest fears, resulting in a evocative tapestry of emotions.", + "genres": [ + "Self-Help", + "Graphic Novel", + "Philosophy" + ], + "metadata": { + "isbn": "978-1-228-26808-3", + "language": "Korean", + "edition": "First" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Into Tides Under the Sky", + "author": "Rachel Jacobson", + "number_of_pages": 223, + "rating": 1.2, + "publication_year": 2010, + "summary": "Set in a magical forest, Rachel Jacobson's 'Into Tides Under the Sky' captures the essence of an unraveling conspiracy, with characters struggling through making impossible sacrifices. a philosophical meditation awaits readers.", + "genres": [ + "Satire", + "Young Adult", + "Philosophy", + "Poetry" + ], + "metadata": { + "isbn": "978-0-513-57979-8", + "language": "Italian", + "edition": "Anniversary Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Beyond Waves in the Universe", + "author": "Nicholas Snyder", + "number_of_pages": 717, + "rating": 1.9, + "publication_year": 2002, + "summary": "'Beyond Waves in the Universe' by Nicholas Snyder unfolds in an ancient underwater city, where a journey to the edge of the world sets the stage. Characters navigate defending a fragile hope in an inventive flight of fancy.", + "genres": [ + "Science Fiction", + "Self-Help", + "Mystery" + ], + "metadata": { + "isbn": "978-1-4879-8163-1", + "language": "Chinese", + "edition": "Limited Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Beneath Operas of Lost Souls", + "author": "Benjamin Patterson", + "number_of_pages": 724, + "rating": 1.8, + "publication_year": 1975, + "summary": "Set amidst a hidden library, 'Beneath Operas of Lost Souls' by Benjamin Patterson takes on a battle for the soul, where fulfilling a forgotten prophecy leads to a deeply introspective narrative.", + "genres": [ + "Poetry", + "Satire" + ], + "metadata": { + "isbn": "978-0-13-465453-9", + "language": "Italian", + "edition": "Anniversary Edition" + }, + "is_checked_out": true, + "borrower": "Steve Fernandez", + "due_date": "2024-12-13" + }, + { + "title": "Through Whispers on the Edge", + "author": "Michael Olson", + "number_of_pages": 284, + "rating": 1.9, + "publication_year": 1981, + "summary": "Within the world of a futuristic laboratory, Michael Olson's 'Through Whispers on the Edge' unravels a tale of a fight for honor. Characters must confront rekindling a lost love, resulting in a dark and twisted tale of horror.", + "genres": [ + "Self-Help", + "Romance" + ], + "metadata": { + "isbn": "978-0-934153-59-1", + "language": "Japanese", + "edition": "Limited Edition" + }, + "is_checked_out": true, + "borrower": "Kathleen Fisher", + "due_date": "2024-12-10" + }, + { + "title": "Across Silence and the Ancient Prophecy", + "author": "Eric Fitzgerald", + "number_of_pages": 709, + "rating": 1.3, + "publication_year": 1983, + "summary": "In a barren wasteland, 'Across Silence and the Ancient Prophecy' by Eric Fitzgerald takes readers on a journey through a journey to the afterlife. The characters are challenged by salvaging a shattered trust, creating an inventive flight of fancy.", + "genres": [ + "Science Fiction", + "Romance", + "Young Adult", + "Poetry" + ], + "metadata": { + "isbn": "978-1-06-255199-0", + "language": "Russian", + "edition": "Collector's Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "In Darkness of Brightness", + "author": "Nichole Carlson", + "number_of_pages": 433, + "rating": 3.2, + "publication_year": 1983, + "summary": "Amidst a sprawling metropolis, 'In Darkness of Brightness' by Nichole Carlson delves into a battle against the elements. The characters must overcome rebuilding a kingdom, making this a poignant and bittersweet reflection.", + "genres": [ + "Graphic Novel", + "Poetry", + "Epic" + ], + "metadata": { + "isbn": "978-0-592-30845-6", + "language": "German", + "edition": "Deluxe Edition" + }, + "is_checked_out": true, + "borrower": "Robert Trevino", + "due_date": "2024-11-26" + }, + { + "title": "A Dreams of the Landscape", + "author": "Jennifer Richardson", + "number_of_pages": 779, + "rating": 1.5, + "publication_year": 1992, + "summary": "In a desert wasteland, Jennifer Richardson's 'A Dreams of the Landscape' unravels a gripping tale of a quest for peace. The characters must navigate fulfilling a forgotten promise, creating a dark and brooding story.", + "genres": [ + "Epic", + "Young Adult", + "Historical Fiction", + "Romance" + ], + "metadata": { + "isbn": "978-0-303-61952-9", + "language": "French", + "edition": "Second" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "In Echoes in the Dark", + "author": "Rhonda Barton", + "number_of_pages": 578, + "rating": 2.2, + "publication_year": 2000, + "summary": "In the backdrop of a lost temple, 'In Echoes in the Dark' by Rhonda Barton explores a fight for honor. The characters' journey through unleashing a terrible curse results in a poetic and haunting story.", + "genres": [ + "Comedy", + "Epic" + ], + "metadata": { + "isbn": "978-0-236-54474-5", + "language": "Russian", + "edition": "Anniversary Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "The Silence of Broken Promises", + "author": "Jason Kelly", + "number_of_pages": 737, + "rating": 3.7, + "publication_year": 2013, + "summary": "Jason Kelly's masterpiece, 'The Silence of Broken Promises', is a story set in an isolated lighthouse. Focusing on a struggle against tyranny, the narrative reveals confronting a powerful enemy, delivering a thrilling and action-packed blockbuster.", + "genres": [ + "Drama", + "Memoir", + "Science Fiction", + "Biography" + ], + "metadata": { + "isbn": "978-0-7559-3465-2", + "language": "Arabic", + "edition": "Special Release" + }, + "is_checked_out": true, + "borrower": "Matthew Brown", + "due_date": "2024-12-16" + }, + { + "title": "From the Wind and the Journey", + "author": "Barbara Fisher", + "number_of_pages": 611, + "rating": 2.4, + "publication_year": 2002, + "summary": "In 'From the Wind and the Journey', Barbara Fisher crafts a narrative set in a war-torn kingdom, focusing on a journey to the afterlife. With surviving against all odds, this book is a dark and brooding story.", + "genres": [ + "History", + "Young Adult" + ], + "metadata": { + "isbn": "978-1-996786-98-7", + "language": "German", + "edition": "Limited Edition" + }, + "is_checked_out": true, + "borrower": "Rebecca Wagner", + "due_date": "2024-12-02" + }, + { + "title": "From Moments of Eternal Night", + "author": "Monica Dixon", + "number_of_pages": 404, + "rating": 2.3, + "publication_year": 1985, + "summary": "Set in a hidden library, Monica Dixon's 'From Moments of Eternal Night' captures the essence of a journey through time, with characters struggling through avenging a terrible wrong. a thought-provoking exploration of the human condition awaits readers.", + "genres": [ + "Thriller", + "Science Fiction" + ], + "metadata": { + "isbn": "978-1-174-78336-4", + "language": "Spanish", + "edition": "Third" + }, + "is_checked_out": true, + "borrower": "Thomas Kramer", + "due_date": "2024-12-03" + }, + { + "title": "A Legends Through Time", + "author": "Colleen Johns", + "number_of_pages": 621, + "rating": 2.3, + "publication_year": 1994, + "summary": "'A Legends Through Time' is Colleen Johns's brilliant novel, set in a haunted shack. Exploring a journey of self-discovery, the characters face finding a way home, resulting in a whimsical and enchanting fable.", + "genres": [ + "Horror", + "Philosophy", + "Mystery" + ], + "metadata": { + "isbn": "978-0-691-69112-1", + "language": "French", + "edition": "Limited Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "In Paths of the Past", + "author": "Antonio Juarez", + "number_of_pages": 517, + "rating": 1.2, + "publication_year": 2008, + "summary": "In the backdrop of a moonlit battlefield, 'In Paths of the Past' by Antonio Juarez explores a quest for revenge. The characters' journey through reawakening a lost memory results in a thrilling and action-packed blockbuster.", + "genres": [ + "Self-Help", + "Horror" + ], + "metadata": { + "isbn": "978-0-323-81383-9", + "language": "Italian", + "edition": "First" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Among Legends and the Last Frontier", + "author": "Randy Ellis", + "number_of_pages": 756, + "rating": 3.9, + "publication_year": 1986, + "summary": "'Among Legends and the Last Frontier' is Randy Ellis's brilliant novel, set in a crumbling mansion. Exploring a fight against fate, the characters face unlocking a hidden power, resulting in a deeply introspective narrative.", + "genres": [ + "Romance", + "Dystopian", + "Self-Help", + "Horror" + ], + "metadata": { + "isbn": "978-1-55510-079-7", + "language": "Italian", + "edition": "Illustrated Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Into Tides of a Lifetime", + "author": "Jon Hill", + "number_of_pages": 716, + "rating": 1.3, + "publication_year": 2001, + "summary": "'Into Tides of a Lifetime', a novel by Jon Hill, is set in an ancient underwater city, where a fight against fate unfolds. The characters grapple with defying a cruel fate, creating a gripping tale of suspense.", + "genres": [ + "Dystopian" + ], + "metadata": { + "isbn": "978-0-09-408237-3", + "language": "Spanish", + "edition": "Deluxe Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Hidden Paths of Eternal Night", + "author": "Samantha Norris", + "number_of_pages": 579, + "rating": 3.5, + "publication_year": 2001, + "summary": "In 'Hidden Paths of Eternal Night', Samantha Norris crafts a narrative set in a barren wasteland, focusing on a struggle for power. With defying a tyrant, this book is a suspenseful page-turner.", + "genres": [ + "Fantasy", + "Science Fiction" + ], + "metadata": { + "isbn": "978-0-18-203978-4", + "language": "Arabic", + "edition": "Special Release" + }, + "is_checked_out": true, + "borrower": "Bradley Castaneda", + "due_date": "2024-12-10" + }, + { + "title": "Within Friendships of the Past", + "author": "Patrick Harper", + "number_of_pages": 315, + "rating": 2.6, + "publication_year": 1994, + "summary": "In Patrick Harper's 'Within Friendships of the Past', the setting of a secret base in the Arctic becomes a stage for a journey of self-discovery. The narrative weaves through unleashing a terrible weapon, offering a chilling and atmospheric tale.", + "genres": [ + "Romance", + "Fantasy", + "Dystopian", + "Drama" + ], + "metadata": { + "isbn": "978-1-05-715850-0", + "language": "English", + "edition": "Anniversary Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Into Moments of Broken Promises", + "author": "Gerald Hill", + "number_of_pages": 617, + "rating": 4.0, + "publication_year": 2002, + "summary": "'Into Moments of Broken Promises', written by Gerald Hill, is a story set in a desert wasteland. Delving into a quest for peace, the characters tackle salvaging a shattered trust, creating a tour de force.", + "genres": [ + "Comedy" + ], + "metadata": { + "isbn": "978-0-517-99501-3", + "language": "Chinese", + "edition": "Second" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Beneath Moments in the Universe", + "author": "Hector Ramirez", + "number_of_pages": 255, + "rating": 1.1, + "publication_year": 1977, + "summary": "Hector Ramirez's masterpiece, 'Beneath Moments in the Universe', is a story set in an oasis of art. Focusing on a search for inner peace, the narrative reveals reclaiming a stolen legacy, delivering a rich and immersive world.", + "genres": [ + "Satire", + "Drama" + ], + "metadata": { + "isbn": "978-0-624-45317-8", + "language": "Spanish", + "edition": "Illustrated Edition" + }, + "is_checked_out": true, + "borrower": "Jason Blake", + "due_date": "2024-12-20" + }, + { + "title": "Hidden Notes of the Future", + "author": "Christina Hernandez", + "number_of_pages": 441, + "rating": 3.1, + "publication_year": 1991, + "summary": "Set in an underwater cave, Christina Hernandez's 'Hidden Notes of the Future' captures the essence of a battle for the ages, with characters struggling through unleashing a terrible weapon. a whimsical and enchanting fable awaits readers.", + "genres": [ + "Comedy", + "Graphic Novel" + ], + "metadata": { + "isbn": "978-1-284-81716-4", + "language": "French", + "edition": "Special Release" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Beneath Light on the Edge", + "author": "Heather Morales", + "number_of_pages": 698, + "rating": 1.5, + "publication_year": 2003, + "summary": "Amidst a small, quiet village, 'Beneath Light on the Edge' by Heather Morales delves into a battle for freedom. The characters must overcome salvaging a shattered trust, making this a deeply introspective narrative.", + "genres": [ + "Dystopian" + ], + "metadata": { + "isbn": "978-1-5295-8182-9", + "language": "Japanese", + "edition": "Limited Edition" + }, + "is_checked_out": true, + "borrower": "Caroline White", + "due_date": "2024-12-09" + }, + { + "title": "Among Mirrors of Eternal Night", + "author": "Sarah Olson", + "number_of_pages": 694, + "rating": 3.5, + "publication_year": 2003, + "summary": "In 'Among Mirrors of Eternal Night', Sarah Olson sets a story of a quest for redemption against the vivid backdrop of a lush garden. The characters' struggles with rediscovering a lost friendship result in a deeply moving story.", + "genres": [ + "Historical Fiction", + "Thriller", + "Graphic Novel" + ], + "metadata": { + "isbn": "978-0-543-47061-4", + "language": "Portuguese", + "edition": "Second" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Within Silence of the Past", + "author": "Victoria Holt", + "number_of_pages": 591, + "rating": 1.0, + "publication_year": 1981, + "summary": "In Victoria Holt's 'Within Silence of the Past', set in an isolated lighthouse, readers explore a struggle against destiny. Faced with reconciling a bitter rivalry, the story delivers a masterful blend of tension and hope.", + "genres": [ + "Non-Fiction", + "Fantasy", + "Historical Fiction", + "Tragedy" + ], + "metadata": { + "isbn": "978-1-04-230685-5", + "language": "Arabic", + "edition": "Second" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Across Silence on the Edge", + "author": "Lisa Martinez", + "number_of_pages": 372, + "rating": 2.0, + "publication_year": 1985, + "summary": "Set amidst a small, quiet village, 'Across Silence on the Edge' by Lisa Martinez takes on a fight against fate, where unleashing a terrible curse leads to a tour de force.", + "genres": [ + "Graphic Novel", + "Epic", + "Self-Help" + ], + "metadata": { + "isbn": "978-0-06-126190-9", + "language": "Korean", + "edition": "Second" + }, + "is_checked_out": true, + "borrower": "Debbie Thornton", + "due_date": "2024-11-25" + }, + { + "title": "From Ghosts and the Journey", + "author": "Toni Figueroa", + "number_of_pages": 303, + "rating": 4.0, + "publication_year": 1975, + "summary": "'From Ghosts and the Journey' by Toni Figueroa immerses readers in a desert wasteland, where a clash of cultures collides with escaping from relentless pursuers, delivering a thrilling and fast-paced adventure.", + "genres": [ + "Romance", + "Philosophy", + "Self-Help", + "Satire" + ], + "metadata": { + "isbn": "978-0-939649-88-4", + "language": "French", + "edition": "Second" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Among Moons of the Landscape", + "author": "Phillip Garner", + "number_of_pages": 760, + "rating": 4.6, + "publication_year": 2007, + "summary": "In a lush garden, Phillip Garner's 'Among Moons of the Landscape' tells a tale of a battle for freedom. The narrative unfolds as the characters navigate unlocking a hidden power, offering a lighthearted romp.", + "genres": [ + "Graphic Novel", + "Horror", + "Poetry", + "Dystopian" + ], + "metadata": { + "isbn": "978-0-01-177499-2", + "language": "Japanese", + "edition": "Illustrated Edition" + }, + "is_checked_out": true, + "borrower": "Richard Ford", + "due_date": "2024-11-29" + }, + { + "title": "From Acquaintances and the Ancient Prophecy", + "author": "John Williams", + "number_of_pages": 622, + "rating": 4.2, + "publication_year": 2008, + "summary": "'From Acquaintances and the Ancient Prophecy' by John Williams immerses readers in a lost temple, where a battle for the future collides with reviving a shattered empire, delivering an unforgettable and immersive experience.", + "genres": [ + "Non-Fiction" + ], + "metadata": { + "isbn": "978-1-888696-43-1", + "language": "Arabic", + "edition": "Deluxe Edition" + }, + "is_checked_out": true, + "borrower": "Steven Blankenship", + "due_date": "2024-12-12" + }, + { + "title": "Beneath Mirrors of Tomorrow", + "author": "Dana Lopez", + "number_of_pages": 594, + "rating": 4.4, + "publication_year": 2013, + "summary": "Dana Lopez's masterpiece, 'Beneath Mirrors of Tomorrow', is a story set in a desert wasteland. Focusing on a search for answers, the narrative reveals questioning their identity, delivering a poignant and bittersweet reflection.", + "genres": [ + "Comedy", + "Horror", + "Poetry" + ], + "metadata": { + "isbn": "978-1-5493-1582-4", + "language": "Arabic", + "edition": "Second" + }, + "is_checked_out": true, + "borrower": "Anna Sharp", + "due_date": "2024-11-26" + }, + { + "title": "The Stars of Tomorrow", + "author": "Julie Wright", + "number_of_pages": 584, + "rating": 1.7, + "publication_year": 1982, + "summary": "In Julie Wright's 'The Stars of Tomorrow', the setting of a haunted shack becomes a stage for a search for purpose. The narrative weaves through defending a dream, offering a heartrending saga of love and loss.", + "genres": [ + "Mystery" + ], + "metadata": { + "isbn": "978-0-903964-68-5", + "language": "Russian", + "edition": "Illustrated Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "The Echoes of the Heart", + "author": "Miguel Wagner", + "number_of_pages": 314, + "rating": 2.5, + "publication_year": 1979, + "summary": "Within a sprawling museum, 'The Echoes of the Heart' by Miguel Wagner captures the spirit of a forbidden love affair. The story builds as characters confront rekindling a lost passion, offering a whimsical and enchanting fable.", + "genres": [ + "Adventure", + "Fantasy", + "Comedy", + "Science Fiction" + ], + "metadata": { + "isbn": "978-1-69653-095-8", + "language": "Portuguese", + "edition": "Limited Edition" + }, + "is_checked_out": true, + "borrower": "Priscilla Joseph", + "due_date": "2024-12-01" + }, + { + "title": "Across the Wind in the Dark", + "author": "Zachary Marshall", + "number_of_pages": 777, + "rating": 4.6, + "publication_year": 1990, + "summary": "Amidst a lost temple, 'Across the Wind in the Dark' by Zachary Marshall delves into a journey of self-discovery. The characters must overcome rebuilding lost trust, making this a dazzling epic of wonder and imagination.", + "genres": [ + "Historical Fiction" + ], + "metadata": { + "isbn": "978-0-06-738327-8", + "language": "Chinese", + "edition": "Second" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Hidden Darkness that Changed Everything", + "author": "Michael Rocha", + "number_of_pages": 349, + "rating": 5.0, + "publication_year": 2014, + "summary": "Set in a forgotten metropolis, 'Hidden Darkness that Changed Everything' by Michael Rocha dives into a struggle against darkness. Facing facing their greatest fears, this is a light and breezy beach read.", + "genres": [ + "Mystery", + "Fantasy", + "Biography" + ], + "metadata": { + "isbn": "978-0-308-04093-9", + "language": "German", + "edition": "Deluxe Edition" + }, + "is_checked_out": true, + "borrower": "John Klein MD", + "due_date": "2024-11-25" + }, + { + "title": "In Legends of the Future", + "author": "Amber May", + "number_of_pages": 483, + "rating": 1.7, + "publication_year": 2005, + "summary": "In 'In Legends of the Future', Amber May sets a story of an exploration of forbidden knowledge against the vivid backdrop of an oasis of art. The characters' struggles with questioning their identity result in a thrilling and fast-paced adventure.", + "genres": [ + "Non-Fiction", + "Fantasy", + "Satire", + "Mystery" + ], + "metadata": { + "isbn": "978-0-06-364971-2", + "language": "Russian", + "edition": "Anniversary Edition" + }, + "is_checked_out": true, + "borrower": "Alicia Torres", + "due_date": "2024-12-17" + }, + { + "title": "Hidden Echoes of a Lifetime", + "author": "Joseph Dominguez", + "number_of_pages": 275, + "rating": 2.8, + "publication_year": 2024, + "summary": "In 'Hidden Echoes of a Lifetime', Joseph Dominguez sets a story of a journey of self-discovery against the vivid backdrop of a distant alien world. The characters' struggles with finding a way home result in a rich and evocative tapestry of words.", + "genres": [ + "Philosophy", + "Young Adult", + "Mystery", + "Biography" + ], + "metadata": { + "isbn": "978-1-895170-83-2", + "language": "German", + "edition": "Deluxe Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Through Tides of Yesterday", + "author": "Travis Murphy", + "number_of_pages": 535, + "rating": 2.6, + "publication_year": 1996, + "summary": "In 'Through Tides of Yesterday', Travis Murphy weaves a tale set in a haunted shack, centered around a clash of cultures. With fulfilling a forgotten destiny, the story becomes a deeply moving story.", + "genres": [ + "Satire" + ], + "metadata": { + "isbn": "978-0-237-35730-6", + "language": "Korean", + "edition": "Illustrated Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "In Legends of Lost Souls", + "author": "Frank Washington", + "number_of_pages": 439, + "rating": 1.4, + "publication_year": 1988, + "summary": "In Frank Washington's 'In Legends of Lost Souls', the setting of a hidden library becomes a stage for a fight for justice. The narrative weaves through unleashing a terrible weapon, offering a dazzling epic of wonder and imagination.", + "genres": [ + "Tragedy" + ], + "metadata": { + "isbn": "978-0-220-33316-4", + "language": "English", + "edition": "Illustrated Edition" + }, + "is_checked_out": true, + "borrower": "Dominic Carter", + "due_date": "2024-12-20" + }, + { + "title": "From the Horizon of Lost Souls", + "author": "Jeffery Parker", + "number_of_pages": 627, + "rating": 3.4, + "publication_year": 1984, + "summary": "Set amidst a remote island, 'From the Horizon of Lost Souls' by Jeffery Parker takes on a journey through time, where fulfilling a forgotten destiny leads to a heartrending saga of love and loss.", + "genres": [ + "Horror" + ], + "metadata": { + "isbn": "978-1-4489-3322-8", + "language": "Japanese", + "edition": "Limited Edition" + }, + "is_checked_out": true, + "borrower": "Robin Pham", + "due_date": "2024-12-12" + }, + { + "title": "Through Operas of Lost Souls", + "author": "Tyler Fitzgerald", + "number_of_pages": 427, + "rating": 4.0, + "publication_year": 2012, + "summary": "Within a secret base in the Arctic, 'Through Operas of Lost Souls' by Tyler Fitzgerald captures the spirit of a quest for the truth. The story builds as characters confront salvaging a shattered trust, offering a chilling and atmospheric tale.", + "genres": [ + "Memoir", + "Drama", + "Philosophy" + ], + "metadata": { + "isbn": "978-0-283-73842-5", + "language": "Japanese", + "edition": "Anniversary Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Among Legends of the Future", + "author": "Mark Vasquez", + "number_of_pages": 366, + "rating": 2.3, + "publication_year": 2013, + "summary": "'Among Legends of the Future' by Mark Vasquez immerses readers in an underwater cave, where a battle for the future collides with escaping from relentless pursuers, delivering a deep character study.", + "genres": [ + "Poetry" + ], + "metadata": { + "isbn": "978-0-7792-2720-4", + "language": "Arabic", + "edition": "First" + }, + "is_checked_out": true, + "borrower": "Susan Williams", + "due_date": "2024-12-02" + }, + { + "title": "Across Stars and the Unknown", + "author": "Scott Russell", + "number_of_pages": 602, + "rating": 4.6, + "publication_year": 2011, + "summary": "'Across Stars and the Unknown' by Scott Russell is set in a remote island and tells a story of a quest for vengeance. The protagonists face defeating a powerful adversary, in a tale that delivers a tour de force.", + "genres": [ + "Dystopian" + ], + "metadata": { + "isbn": "978-1-916911-10-9", + "language": "German", + "edition": "Illustrated Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "From Ashes of the Heart", + "author": "Jason Duncan", + "number_of_pages": 198, + "rating": 2.9, + "publication_year": 1994, + "summary": "Set against a remote island, 'From Ashes of the Heart' by Jason Duncan unveils a quest for redemption. The story builds through defying a tyrant, offering a deep character study.", + "genres": [ + "Horror" + ], + "metadata": { + "isbn": "978-0-8401-0949-1", + "language": "Italian", + "edition": "Anniversary Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Beneath Waves Through Time", + "author": "Kristin Garrett", + "number_of_pages": 296, + "rating": 3.2, + "publication_year": 1994, + "summary": "In 'Beneath Waves Through Time', Kristin Garrett crafts a narrative set in a bustling marketplace, focusing on a journey of self-discovery. With solving a baffling mystery, this book is a pulse-pounding thrill ride.", + "genres": [ + "Biography", + "History", + "Poetry", + "Thriller" + ], + "metadata": { + "isbn": "978-0-7701-5322-9", + "language": "German", + "edition": "Anniversary Edition" + }, + "is_checked_out": true, + "borrower": "Steven Smith", + "due_date": "2024-11-29" + }, + { + "title": "Beyond Dreams and Forgotten Worlds", + "author": "James Lee", + "number_of_pages": 395, + "rating": 1.7, + "publication_year": 2007, + "summary": "'Beyond Dreams and Forgotten Worlds' by James Lee immerses readers in a remote island, where a search for answers collides with unlocking a hidden power, delivering a evocative tapestry of emotions.", + "genres": [ + "Biography", + "Non-Fiction", + "Satire" + ], + "metadata": { + "isbn": "978-1-06-454125-8", + "language": "Russian", + "edition": "Limited Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Under Whispers of Broken Promises", + "author": "Eduardo Gibbs", + "number_of_pages": 375, + "rating": 4.6, + "publication_year": 2002, + "summary": "In Eduardo Gibbs's 'Under Whispers of Broken Promises', the setting of a hidden library becomes a stage for a clash of cultures. The narrative weaves through facing their greatest fears, offering a thought-provoking exploration of the human condition.", + "genres": [ + "Philosophy", + "Self-Help" + ], + "metadata": { + "isbn": "978-0-325-25094-6", + "language": "French", + "edition": "Limited Edition" + }, + "is_checked_out": true, + "borrower": "Charles Barber", + "due_date": "2024-12-14" + }, + { + "title": "Among Dreams and the Journey", + "author": "Melissa Wyatt", + "number_of_pages": 574, + "rating": 4.7, + "publication_year": 2009, + "summary": "'Among Dreams and the Journey' by Melissa Wyatt unfolds in a crumbling mansion, where a struggle against time sets the stage. Characters navigate facing their greatest fears in a dark and brooding story.", + "genres": [ + "Science Fiction", + "Romance", + "Dystopian" + ], + "metadata": { + "isbn": "978-1-177-04903-0", + "language": "Russian", + "edition": "Special Release" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Beyond Notes of Tomorrow", + "author": "Elizabeth Perez", + "number_of_pages": 436, + "rating": 2.6, + "publication_year": 1984, + "summary": "Within the world of a forgotten metropolis, Elizabeth Perez's 'Beyond Notes of Tomorrow' unravels a tale of a race to uncover ancient secrets. Characters must confront defying a cruel god, resulting in a light and breezy beach read.", + "genres": [ + "Fantasy", + "Memoir", + "Non-Fiction" + ], + "metadata": { + "isbn": "978-1-992435-32-2", + "language": "Russian", + "edition": "Deluxe Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Within Legends and the Journey", + "author": "Peter Dixon", + "number_of_pages": 741, + "rating": 3.1, + "publication_year": 2019, + "summary": "Within a bustling marketplace, 'Within Legends and the Journey' by Peter Dixon captures the spirit of a fight for survival. The story builds as characters confront overcoming their own doubts, offering a gripping tale of suspense.", + "genres": [ + "Romance", + "Adventure", + "Philosophy" + ], + "metadata": { + "isbn": "978-0-03-199377-9", + "language": "Russian", + "edition": "Limited Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Under Secrets Under the Sky", + "author": "Ryan Phillips", + "number_of_pages": 360, + "rating": 2.9, + "publication_year": 2011, + "summary": "In Ryan Phillips's 'Under Secrets Under the Sky', set in a barren wasteland, readers explore a race to uncover ancient secrets. Faced with unleashing a terrible weapon, the story delivers a thrilling and fast-paced adventure.", + "genres": [ + "History", + "Non-Fiction", + "Philosophy", + "Poetry" + ], + "metadata": { + "isbn": "978-1-101-97526-8", + "language": "English", + "edition": "Third" + }, + "is_checked_out": true, + "borrower": "Lisa Turner", + "due_date": "2024-12-09" + }, + { + "title": "Hidden Secrets and the Ancient Prophecy", + "author": "Julie Thompson", + "number_of_pages": 700, + "rating": 1.3, + "publication_year": 2016, + "summary": "In Julie Thompson's 'Hidden Secrets and the Ancient Prophecy', the setting of an isolated lighthouse becomes a stage for an unraveling conspiracy. The narrative weaves through navigating treacherous terrain, offering a thrilling and action-packed blockbuster.", + "genres": [ + "Dystopian" + ], + "metadata": { + "isbn": "978-0-05-478884-0", + "language": "Russian", + "edition": "Limited Edition" + }, + "is_checked_out": true, + "borrower": "Haley Macias", + "due_date": "2024-11-22" + }, + { + "title": "Into Moments of the Landscape", + "author": "Wesley Hamilton", + "number_of_pages": 430, + "rating": 2.9, + "publication_year": 1999, + "summary": "Wesley Hamilton's masterpiece, 'Into Moments of the Landscape', is a story set in a desert wasteland. Focusing on a quest for the truth, the narrative reveals navigating treacherous terrain, delivering a light and breezy beach read.", + "genres": [ + "Satire", + "Science Fiction", + "History" + ], + "metadata": { + "isbn": "978-1-4294-2654-1", + "language": "Korean", + "edition": "Collector's Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Hidden the Horizon of the Heart", + "author": "Ashley Beltran", + "number_of_pages": 316, + "rating": 2.0, + "publication_year": 1981, + "summary": "Within the world of an isolated lighthouse, Ashley Beltran's 'Hidden the Horizon of the Heart' unravels a tale of a fight for honor. Characters must confront avenging a terrible wrong, resulting in a deep character study.", + "genres": [ + "Drama", + "Romance", + "Philosophy" + ], + "metadata": { + "isbn": "978-1-143-40842-7", + "language": "Arabic", + "edition": "First" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Into Darkness on the Edge", + "author": "Caitlin Leon", + "number_of_pages": 449, + "rating": 2.2, + "publication_year": 2004, + "summary": "In Caitlin Leon's 'Into Darkness on the Edge', set in a magical forest, readers explore a search for inner peace. Faced with defying a cruel fate, the story delivers an unforgettable and immersive experience.", + "genres": [ + "Epic", + "Fantasy" + ], + "metadata": { + "isbn": "978-0-89557-521-0", + "language": "German", + "edition": "Second" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Into Darkness on the Edge", + "author": "Theodore Brown", + "number_of_pages": 723, + "rating": 2.0, + "publication_year": 2017, + "summary": "Set in a magical forest, 'Into Darkness on the Edge' by Theodore Brown dives into an unraveling conspiracy. Facing solving a baffling mystery, this is a gripping tale of suspense.", + "genres": [ + "Thriller", + "Mystery", + "Tragedy", + "Romance" + ], + "metadata": { + "isbn": "978-0-353-53711-8", + "language": "Spanish", + "edition": "Anniversary Edition" + }, + "is_checked_out": true, + "borrower": "Elizabeth Casey", + "due_date": "2024-11-25" + }, + { + "title": "A Moons that Changed Everything", + "author": "Brian Torres", + "number_of_pages": 795, + "rating": 1.4, + "publication_year": 1983, + "summary": "Amidst a distant alien world, 'A Moons that Changed Everything' by Brian Torres delves into a journey to the afterlife. The characters must overcome reclaiming a stolen legacy, making this a heartwarming journey.", + "genres": [ + "Biography", + "Adventure", + "Philosophy", + "Epic" + ], + "metadata": { + "isbn": "978-0-7893-9962-5", + "language": "Italian", + "edition": "Second" + }, + "is_checked_out": true, + "borrower": "Robert Ross", + "due_date": "2024-11-22" + }, + { + "title": "Into Silence in the Universe", + "author": "Hayley Thompson", + "number_of_pages": 524, + "rating": 2.1, + "publication_year": 2008, + "summary": "'Into Silence in the Universe' is Hayley Thompson's latest masterpiece set in a distant alien world. With a focus on a quest for redemption, the story brings to life deciphering cryptic messages, delivering a lighthearted romp.", + "genres": [ + "Historical Fiction", + "Self-Help", + "Dystopian" + ], + "metadata": { + "isbn": "978-1-57695-980-0", + "language": "English", + "edition": "Illustrated Edition" + }, + "is_checked_out": true, + "borrower": "Jeanne Tucker", + "due_date": "2024-12-08" + }, + { + "title": "Beneath Phantoms of the Past", + "author": "Kevin Santiago", + "number_of_pages": 401, + "rating": 1.4, + "publication_year": 2018, + "summary": "'Beneath Phantoms of the Past' by Kevin Santiago takes place in a hidden library, where an unsolved mystery shapes the characters' journey through reclaiming a stolen legacy. a deep character study.", + "genres": [ + "Biography", + "Adventure", + "Satire" + ], + "metadata": { + "isbn": "978-1-4850-4091-0", + "language": "French", + "edition": "Second" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Through Ashes of the Future", + "author": "Rebecca Watson", + "number_of_pages": 385, + "rating": 1.4, + "publication_year": 1981, + "summary": "Set against a vibrant carnival, 'Through Ashes of the Future' by Rebecca Watson unveils a fight for honor. The story builds through making impossible sacrifices, offering a poignant and bittersweet reflection.", + "genres": [ + "History", + "Romance", + "Satire", + "Poetry" + ], + "metadata": { + "isbn": "978-1-5380-0152-3", + "language": "Portuguese", + "edition": "First" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Against Whispers of Yesterday", + "author": "Megan Reed", + "number_of_pages": 325, + "rating": 3.0, + "publication_year": 1994, + "summary": "'Against Whispers of Yesterday', written by Megan Reed, paints a vivid picture of a barren wasteland. Exploring a struggle against tyranny, the story revolves around confronting a powerful enemy, offering a dark and twisted tale of horror.", + "genres": [ + "Epic" + ], + "metadata": { + "isbn": "978-0-02-315141-5", + "language": "Portuguese", + "edition": "Third" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Under Waves of the Landscape", + "author": "Aaron Morris", + "number_of_pages": 774, + "rating": 2.4, + "publication_year": 2019, + "summary": "'Under Waves of the Landscape' is Aaron Morris's latest masterpiece set in a lost temple. With a focus on a battle for the ages, the story brings to life defending a dream, delivering a pulse-pounding thrill ride.", + "genres": [ + "Mystery", + "Science Fiction", + "Self-Help" + ], + "metadata": { + "isbn": "978-0-493-30631-5", + "language": "Italian", + "edition": "Second" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "In Waves of a Lifetime", + "author": "Richard Rocha", + "number_of_pages": 591, + "rating": 2.2, + "publication_year": 2008, + "summary": "In 'In Waves of a Lifetime', Richard Rocha sets a story of a fight for love against the vivid backdrop of a distant alien world. The characters' struggles with unleashing a terrible force result in a philosophical meditation.", + "genres": [ + "Romance", + "Self-Help" + ], + "metadata": { + "isbn": "978-1-393-88060-8", + "language": "English", + "edition": "Deluxe Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "In Stars of Eternal Night", + "author": "Chris Byrd", + "number_of_pages": 719, + "rating": 4.0, + "publication_year": 1983, + "summary": "'In Stars of Eternal Night', written by Chris Byrd, paints a vivid picture of a secret base in the Arctic. Exploring a fight for honor, the story revolves around challenging a corrupt system, offering a heartrending saga of love and loss.", + "genres": [ + "Fantasy", + "History", + "Philosophy" + ], + "metadata": { + "isbn": "978-1-09-264432-7", + "language": "German", + "edition": "Collector's Edition" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Into Reflections and the Journey", + "author": "Daniel Wheeler", + "number_of_pages": 428, + "rating": 3.1, + "publication_year": 2005, + "summary": "In the backdrop of a mysterious castle, 'Into Reflections and the Journey' by Daniel Wheeler explores a battle for the ages. The characters' journey through rebuilding a kingdom results in an uplifting tale.", + "genres": [ + "Thriller", + "Mystery", + "History" + ], + "metadata": { + "isbn": "978-1-4791-5224-7", + "language": "Korean", + "edition": "Third" + }, + "is_checked_out": false, + "borrower": null, + "due_date": null + }, + { + "title": "Within Voices of the Past", + "author": "Cynthia Rivera", + "number_of_pages": 435, + "rating": 1.2, + "publication_year": 2004, + "summary": "In 'Within Voices of the Past', Cynthia Rivera crafts a narrative set in a barren wasteland, focusing on an unraveling conspiracy. With redeeming a shameful past, this book is a whimsical and enchanting fable.", + "genres": [ + "Poetry" + ], + "metadata": { + "isbn": "978-1-59334-122-0", + "language": "German", + "edition": "Anniversary Edition" + }, + "is_checked_out": true, + "borrower": "Allison Hernandez", + "due_date": "2024-11-28" + } +] \ No newline at end of file diff --git a/test/DataStax.AstraDB.DataApi.IntegrationTests/readme.md b/test/DataStax.AstraDB.DataApi.IntegrationTests/readme.md index c8586c9..2ba0557 100644 --- a/test/DataStax.AstraDB.DataApi.IntegrationTests/readme.md +++ b/test/DataStax.AstraDB.DataApi.IntegrationTests/readme.md @@ -4,11 +4,9 @@ Set the following environment variables before running: ``` export ASTRA_DB_TOKEN="your_token_here" export ASTRA_DB_URL="your_db_url_here" +export ASTRA_DB_DATABASE_NAME="your_db_name" ``` -dotnet test -- "xUnit.MaxParallelThreads=4" +dotnet test -p:TestTfmsInParallel=false -- "xUnit.MaxParallelThreads=1" -reportgenerator --reports:"Path\To\TestProject\TestResults\{guid}\coverage.cobertura.xml" --targetdir:"coveragereport" --reporttypes:Html \ No newline at end of file +dotnet test --filter "FullyQualifiedName~DataStax.AstraDB.DataApi.IntegrationTests.CollectionTests" \ No newline at end of file