diff --git a/CHANGELOG.md b/CHANGELOG.md index e4964c33..6fa7495e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,46 @@ This release drops support for Ruby 2.7. [#453] @flavorjones ### Removed - Remove `SQLite3::VersionProxy` which has been deprecated since v1.3.2. [#453] @flavorjones +- Remove `SQLite3::Translator` and all related type translation methods. + If you need to do type translation on values returned from the statement object, + please wrap it with a delegate object. Here is an example of using a delegate + class to implement type translation: + +```ruby +require "sqlite3" +require "delegate" + +db = SQLite3::Database.new(":memory:") + +return_value = db.execute_batch2 <<-EOSQL + CREATE TABLE items (id integer PRIMARY KEY AUTOINCREMENT, name string); + INSERT INTO items (name) VALUES ("foo"); + INSERT INTO items (name) VALUES ("bar"); +EOSQL + +class MyTranslator < DelegateClass(SQLite3::Statement) + def step + row = super + return if done? + + row.map.with_index do |item, i| + case types[i] + when "integer" # turn all integers to floats + item.to_f + when "string" # add "hello" to all strings + item + "hello" + end + end + end +end + +db.prepare("SELECT * FROM items") do |stmt| + stmt = MyTranslator.new(stmt) + while row = stmt.step + p row + end +end +``` ## 1.7.0 / 2023-12-27 diff --git a/FAQ.md b/FAQ.md index 6f2e2468..63d7e2f7 100644 --- a/FAQ.md +++ b/FAQ.md @@ -289,49 +289,6 @@ by column name, even though they are still arrays! end ``` -## I'd like the values from a query to be the correct types, instead of String. - -You can turn on "type translation" by setting `Database#type_translation` to -true: - - -```ruby - db.type_translation = true - db.execute( "select * from table" ) do |row| - p row - end -``` - - -By doing this, each return value for each row will be translated to its -correct type, based on its declared column type. - - -You can even declare your own translation routines, if (for example) you are -using an SQL type that is not handled by default: - - -```ruby - # assume "objects" table has the following schema: - # create table objects ( - # name varchar2(20), - # thing object - # ) - - db.type_translation = true - db.translator.add_translator( "object" ) do |type, value| - db.decode( value ) - end - - h = { :one=>:two, "three"=>"four", 5=>6 } - dump = db.encode( h ) - - db.execute( "insert into objects values ( ?, ? )", "bob", dump ) - - obj = db.get_first_value( "select thing from objects where name='bob'" ) - p obj == h -``` - ## How do I insert binary data into the database? Use blobs. Blobs are new features of SQLite3. You have to use bind diff --git a/Gemfile b/Gemfile index 7c31addd..bd413c0e 100644 --- a/Gemfile +++ b/Gemfile @@ -15,4 +15,7 @@ group :development do gem "rubocop", require: false gem "standardrb", require: false gem "rubocop-minitest", require: false + + # FIXME: Remove after minitest removes dependency + gem "mutex_m" end diff --git a/lib/sqlite3/database.rb b/lib/sqlite3/database.rb index 50b076e9..91001719 100644 --- a/lib/sqlite3/database.rb +++ b/lib/sqlite3/database.rb @@ -2,7 +2,6 @@ require "sqlite3/errors" require "sqlite3/pragmas" require "sqlite3/statement" -require "sqlite3/translator" require "sqlite3/value" module SQLite3 @@ -82,7 +81,6 @@ def quote(string) # Other supported +options+: # - +:strict+: boolean (default false), disallow the use of double-quoted string literals (see https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted) # - +:results_as_hash+: boolean (default false), return rows as hashes instead of arrays - # - +:type_translation+: boolean (default false), enable type translation # - +:default_transaction_mode+: one of +:deferred+ (default), +:immediate+, or +:exclusive+. If a mode is not specified in a call to #transaction, this will be the default transaction mode. # def initialize file, options = {}, zvfs = nil @@ -124,8 +122,6 @@ def initialize file, options = {}, zvfs = nil @collations = {} @functions = {} @results_as_hash = options[:results_as_hash] - @type_translation = options[:type_translation] - @type_translator = make_type_translator @type_translation @readonly = mode & Constants::Open::READONLY != 0 @default_transaction_mode = options[:default_transaction_mode] || :deferred @@ -145,25 +141,6 @@ def encoding @encoding ||= Encoding.find(execute("PRAGMA encoding").first.first) end - def type_translation= value # :nodoc: - warn(<<~EOWARN) if $VERBOSE - #{caller(1..1).first} is calling `SQLite3::Database#type_translation=` which is deprecated and will be removed in version 2.0.0. - EOWARN - @type_translator = make_type_translator value - @type_translation = value - end - attr_reader :type_translation # :nodoc: - - # Return the type translator employed by this database instance. Each - # database instance has its own type translator; this allows for different - # type handlers to be installed in each instance without affecting other - # instances. Furthermore, the translators are instantiated lazily, so that - # if a database does not use type translation, it will not be burdened by - # the overhead of a useless type translator. (See the Translator class.) - def translator - @translator ||= Translator.new - end - # Installs (or removes) a block that will be invoked for every access # to the database. If the block returns 0 (or +nil+), the statement # is allowed to proceed. Returning 1 causes an authorization error to @@ -741,11 +718,6 @@ def []=(key, value) end end - # Translates a +row+ of data from the database with the given +types+ - def translate_from_db types, row - @type_translator.call types, row - end - # Given a statement, return a result set. # This is not intended for general consumption # :nodoc: @@ -756,21 +728,5 @@ def build_result_set stmt ResultSet.new(self, stmt) end end - - private - - NULL_TRANSLATOR = lambda { |_, row| row } - - def make_type_translator should_translate - if should_translate - lambda { |types, row| - types.zip(row).map do |type, value| - translator.translate(type, value) - end - } - else - NULL_TRANSLATOR - end - end end end diff --git a/lib/sqlite3/resultset.rb b/lib/sqlite3/resultset.rb index 022e2b88..bd9a1ceb 100644 --- a/lib/sqlite3/resultset.rb +++ b/lib/sqlite3/resultset.rb @@ -9,10 +9,6 @@ module SQLite3 class ResultSet include Enumerable - class ArrayWithTypes < Array # :nodoc: - attr_accessor :types - end - class ArrayWithTypesAndFields < Array # :nodoc: attr_writer :types attr_writer :fields @@ -78,9 +74,7 @@ def eof? end # Obtain the next row from the cursor. If there are no more rows to be - # had, this will return +nil+. If type translation is active on the - # corresponding database, the values in the row will be translated - # according to their types. + # had, this will return +nil+. # # The returned value will be an array, unless Database#results_as_hash has # been set to +true+, in which case the returned value will be a hash. @@ -94,19 +88,10 @@ def next row = @stmt.step return nil if @stmt.done? - row = @db.translate_from_db @stmt.types, row - - row = if row.respond_to?(:fields) - # FIXME: this can only happen if the translator returns something - # that responds to `fields`. Since we're removing the translator - # in 2.0, we can remove this branch in 2.0. - ArrayWithTypes.new(row) - else - # FIXME: the `fields` and `types` methods are deprecated on this - # object for version 2.0, so we can safely remove this branch - # as well. - ArrayWithTypesAndFields.new(row) - end + # FIXME: the `fields` and `types` methods are deprecated on this + # object for version 2.0, so we can safely remove this branch + # as well. + row = ArrayWithTypesAndFields.new(row) row.fields = @stmt.columns row.types = @stmt.types @@ -156,10 +141,6 @@ def next_hash row = @stmt.step return nil if @stmt.done? - # FIXME: type translation is deprecated, so this can be removed - # in 2.0 - row = @db.translate_from_db @stmt.types, row - # FIXME: this can be switched to a regular hash in 2.0 row = HashWithTypesAndFields[*@stmt.columns.zip(row).flatten] @@ -172,6 +153,6 @@ def next_hash end class HashResultSet < ResultSet # :nodoc: - alias :next :next_hash + alias_method :next, :next_hash end end diff --git a/lib/sqlite3/translator.rb b/lib/sqlite3/translator.rb deleted file mode 100644 index b7456a26..00000000 --- a/lib/sqlite3/translator.rb +++ /dev/null @@ -1,113 +0,0 @@ -require "time" -require "date" - -module SQLite3 - # The Translator class encapsulates the logic and callbacks necessary for - # converting string data to a value of some specified type. Every Database - # instance may have a Translator instance, in order to assist in type - # translation (Database#type_translation). - # - # Further, applications may define their own custom type translation logic - # by registering translator blocks with the corresponding database's - # translator instance (Database#translator). - class Translator - # Create a new Translator instance. It will be preinitialized with default - # translators for most SQL data types. - def initialize - @translators = Hash.new(proc { |type, value| value }) - @type_name_cache = {} - register_default_translators - end - - # Add a new translator block, which will be invoked to process type - # translations to the given type. The type should be an SQL datatype, and - # may include parentheses (i.e., "VARCHAR(30)"). However, any parenthetical - # information is stripped off and discarded, so type translation decisions - # are made solely on the "base" type name. - # - # The translator block itself should accept two parameters, "type" and - # "value". In this case, the "type" is the full type name (including - # parentheses), so the block itself may include logic for changing how a - # type is translated based on the additional data. The "value" parameter - # is the (string) data to convert. - # - # The block should return the translated value. - def add_translator(type, &block) # :yields: type, value - warn(<<~EOWARN) if $VERBOSE - #{caller(1..1).first} is calling `SQLite3::Translator#add_translator`. Built-in translators are deprecated and will be removed in version 2.0.0. - EOWARN - @translators[type_name(type)] = block - end - - # Translate the given string value to a value of the given type. In the - # absence of an installed translator block for the given type, the value - # itself is always returned. Further, +nil+ values are never translated, - # and are always passed straight through regardless of the type parameter. - def translate(type, value) - unless value.nil? - # FIXME: this is a hack to support Sequel - if type && %w[datetime timestamp].include?(type.downcase) - @translators[type_name(type)].call(type, value.to_s) - else - @translators[type_name(type)].call(type, value) - end - end - end - - # A convenience method for working with type names. This returns the "base" - # type name, without any parenthetical data. - def type_name(type) - @type_name_cache[type] ||= begin - type = "" if type.nil? - type = $1 if type =~ /^(.*?)\(/ - type.upcase - end - end - private :type_name - - # Register the default translators for the current Translator instance. - # This includes translators for most major SQL data types. - def register_default_translators - ["time", - "timestamp"].each { |type| add_translator(type) { |t, v| Time.parse(v) } } - - add_translator("date") { |t, v| Date.parse(v) } - add_translator("datetime") { |t, v| DateTime.parse(v) } - - ["decimal", - "float", - "numeric", - "double", - "real", - "dec", - "fixed"].each { |type| add_translator(type) { |t, v| v.to_f } } - - ["integer", - "smallint", - "mediumint", - "int", - "bigint"].each { |type| add_translator(type) { |t, v| v.to_i } } - - ["bit", - "bool", - "boolean"].each do |type| - add_translator(type) do |t, v| - !(v.strip.gsub(/00+/, "0") == "0" || - v.downcase == "false" || - v.downcase == "f" || - v.downcase == "no" || - v.downcase == "n") - end - end - - add_translator("tinyint") do |type, value| - if /\(\s*1\s*\)/.match?(type) - value.to_i == 1 - else - value.to_i - end - end - end - private :register_default_translators - end -end diff --git a/sqlite3.gemspec b/sqlite3.gemspec index 2afa7521..d0746454 100644 --- a/sqlite3.gemspec +++ b/sqlite3.gemspec @@ -68,7 +68,6 @@ Gem::Specification.new do |s| "lib/sqlite3/pragmas.rb", "lib/sqlite3/resultset.rb", "lib/sqlite3/statement.rb", - "lib/sqlite3/translator.rb", "lib/sqlite3/value.rb", "lib/sqlite3/version.rb", "test/helper.rb", diff --git a/test/test_database.rb b/test/test_database.rb index 1efcbf9f..04f226cd 100644 --- a/test/test_database.rb +++ b/test/test_database.rb @@ -97,26 +97,6 @@ def test_get_first_row assert_equal [1], @db.get_first_row("SELECT 1") end - def test_get_first_row_with_type_translation_and_hash_results - @db.results_as_hash = true - capture_io do # hush translation deprecation warnings - @db.type_translation = true - assert_equal({"1" => 1}, @db.get_first_row("SELECT 1")) - end - end - - def test_execute_with_type_translation_and_hash - rows = [] - @db.results_as_hash = true - - capture_io do # hush translation deprecation warnings - @db.type_translation = true - @db.execute("SELECT 1") { |row| rows << row } - end - - assert_equal({"1" => 1}, rows.first) - end - def test_encoding assert @db.encoding, "database has encoding" end