require "uri" require "log" # The DB module is a unified interface for database access. # Individual database systems are supported by specific database driver shards. # # Available drivers include: # * [crystal-lang/crystal-sqlite3](https://github.com/crystal-lang/crystal-sqlite3) for SQLite # * [crystal-lang/crystal-mysql](https://github.com/crystal-lang/crystal-mysql) for MySQL and MariaDB # * [will/crystal-pg](https://github.com/will/crystal-pg) for PostgreSQL # * [kaukas/crystal-cassandra](https://github.com/kaukas/crystal-cassandra) for Cassandra # # For basic instructions on implementing a new database driver, check `Driver` and the existing drivers. # # DB manages a connection pool. The connection pool can be configured by query parameters to the # connection `URI` as described in `Database`. # # ### Usage # # Assuming `crystal-sqlite3` is included a SQLite3 database can be opened with `#open`. # # ``` # db = DB.open "sqlite3:./path/to/db/file.db" # db.close # ``` # # If a block is given to `#open` the database is closed automatically # # ``` # DB.open "sqlite3:./file.db" do |db| # # work with db # end # db is closed # ``` # # In the code above `db` is a `Database`. Methods available for querying it are described in `QueryMethods`. # # Three kind of statements can be performed: # 1. `Database#exec` waits no response from the database. # 2. `Database#scalar` reads a single value of the response. # 3. `Database#query` returns a ResultSet that allows iteration over the rows in the response and column information. # # All of the above methods allows parametrised query. Either positional or named arguments. # # Check a full working version: # # The following example uses SQLite where `?` indicates the arguments. If PostgreSQL is used `$1`, `$2`, etc. should be used. `crystal-db` does not interpret the statements. # # ``` # require "db" # require "sqlite3" # # DB.open "sqlite3:./file.db" do |db| # # When using the pg driver, use $1, $2, etc. instead of ? # db.exec "create table contacts (name text, age integer)" # db.exec "insert into contacts values (?, ?)", "John Doe", 30 # # args = [] of DB::Any # args << "Sarah" # args << 33 # db.exec "insert into contacts values (?, ?)", args: args # # puts "max age:" # puts db.scalar "select max(age) from contacts" # => 33 # # puts "contacts:" # db.query "select name, age from contacts order by age desc" do |rs| # puts "#{rs.column_name(0)} (#{rs.column_name(1)})" # # => name (age) # rs.each do # puts "#{rs.read(String)} (#{rs.read(Int32)})" # # => Sarah (33) # # => John Doe (30) # end # end # end # ``` # # ### Object mapping # # The `DB::Serializable` module implements a declarative mapping from DB result # sets to Crystal types. module DB Log = ::Log.for(self) # Types supported to interface with database driver. # These can be used in any `ResultSet#read` or any `Database#query` related # method to be used as query parameters TYPES = [Nil, String, Bool, Int32, Int64, Float32, Float64, Time, Bytes] # See `DB::TYPES` in `DB`. `Any` is a union of all types in `DB::TYPES` {% begin %} alias Any = Union({{*TYPES}}) {% end %} # Result of a `#exec` statement. record ExecResult, rows_affected : Int64, last_insert_id : Int64 # :nodoc: def self.driver_class(driver_name) : Driver.class drivers[driver_name]? || raise(ArgumentError.new(%(no driver was registered for the schema "#{driver_name}", did you maybe forget to require the database driver?))) end # Registers a driver class for a given *driver_name*. # Should be called by drivers implementors only. def self.register_driver(driver_name, driver_class : Driver.class) drivers[driver_name] = driver_class end private def self.drivers @@drivers ||= {} of String => Driver.class end # Creates a `Database` pool and opens initial connection(s) as specified in the connection *uri*. # Use `DB#connect` to open a single connection. # # The scheme of the *uri* determines the driver to use. # Connection parameters such as hostname, user, database name, etc. are specified according # to each database driver's specific format. # # The returned database must be closed by `Database#close`. def self.open(uri : URI | String) build_database(uri) end # Same as `#open` but the database is yielded and closed automatically at the end of the block. def self.open(uri : URI | String, &block) db = build_database(uri) begin yield db ensure db.close end end # Opens a connection using the specified *uri*. # The scheme of the *uri* determines the driver to use. # Returned connection must be closed by `Connection#close`. # If a block is used the connection is yielded and closed automatically. def self.connect(uri : URI | String) build_connection(uri) end # :ditto: def self.connect(uri : URI | String, &block) cnn = build_connection(uri) begin yield cnn ensure cnn.close end end private def self.build_database(connection_string : String) build_database(URI.parse(connection_string)) end private def self.build_database(uri : URI) driver = build_driver(uri) params = HTTP::Params.parse(uri.query || "") connection_options = driver.connection_options(params) pool_options = driver.pool_options(params) builder = driver.connection_builder(uri) factory = ->{ builder.build } Database.new(connection_options, pool_options, &factory) end private def self.build_connection(connection_string : String) build_connection(URI.parse(connection_string)) end private def self.build_connection(uri : URI) build_driver(uri).connection_builder(uri).build end private def self.build_driver(uri : URI) driver_class(uri.scheme).new end # :nodoc: def self.fetch_bool(params : HTTP::Params, name, default : Bool) case (value = params[name]?).try &.downcase when nil default when "true" true when "false" false else raise ArgumentError.new(%(invalid "#{value}" value for option "#{name}")) end end end require "./db/pool" require "./db/string_key_cache" require "./db/enumerable_concat" require "./db/query_methods" require "./db/session_methods" require "./db/disposable" require "./db/connection_builder" require "./db/driver" require "./db/statement" require "./db/begin_transaction" require "./db/connection_context" require "./db/connection" require "./db/transaction" require "./db/statement" require "./db/pool_statement" require "./db/database" require "./db/pool_prepared_statement" require "./db/pool_unprepared_statement" require "./db/result_set" require "./db/error" require "./db/mapping" require "./db/serializable"