From d62185867bf35fa946436da24de5e8aa6f1a6227 Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Thu, 12 Mar 2015 21:08:01 -0300 Subject: [PATCH] Documentation and some refactors --- README.md | 16 +++++++ spec/database_spec.cr | 12 +++++ src/sqlite3/column_type.cr | 3 -- src/sqlite3/database.cr | 62 ++++++++++++++++++++++++- src/sqlite3/exception.cr | 2 + src/sqlite3/lib_sqlite3.cr | 12 ++--- src/sqlite3/result_set.cr | 46 ++++++++++++++++-- src/sqlite3/statement.cr | 95 ++++++++++++++++++++++++++------------ src/sqlite3/type.cr | 8 ++++ src/sqlite3/value.cr | 4 ++ 10 files changed, 215 insertions(+), 45 deletions(-) delete mode 100644 src/sqlite3/column_type.cr create mode 100644 src/sqlite3/type.cr create mode 100644 src/sqlite3/value.cr diff --git a/README.md b/README.md index bfa4802..a6ec09e 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ SQLite3 bindings for [Crystal](http://crystal-lang.org/). +**This is a work in progress.** + +[Documentation](http://manastech.github.io/crystal-sqlite3/) + ### Projectfile ```crystal @@ -9,3 +13,15 @@ deps do github "manastech/crystal-sqlite3" end ``` + +### Usage + +``` +require "sqlite3" + +db = SQLite3::Database.new( "data.db" ) +db.execute("select * from table") do |row| + p row +end +db.close +``` diff --git a/spec/database_spec.cr b/spec/database_spec.cr index 00229e3..98d11e8 100644 --- a/spec/database_spec.cr +++ b/spec/database_spec.cr @@ -61,6 +61,18 @@ describe Database do end end + it "gets column types" do + Database.new(":memory:") do |db| + db.execute "create table person (name string, age integer)" + db.execute %(insert into person values ("foo", 10)) + stmt = db.prepare("select * from person") + stmt.execute + stmt.step + stmt.types.should eq([Type::TEXT, Type::INTEGER]) + stmt.close + end + end + it "gets column by name" do Database.new(":memory:") do |db| db.execute "create table person (name string, age integer)" diff --git a/src/sqlite3/column_type.cr b/src/sqlite3/column_type.cr deleted file mode 100644 index edcf124..0000000 --- a/src/sqlite3/column_type.cr +++ /dev/null @@ -1,3 +0,0 @@ -module SQLite3 - alias ColumnType = Nil | Int64 | Float64 | String | Slice(UInt8) -end diff --git a/src/sqlite3/database.cr b/src/sqlite3/database.cr index d099242..e05a8f4 100644 --- a/src/sqlite3/database.cr +++ b/src/sqlite3/database.cr @@ -1,4 +1,18 @@ +# The Database class encapsulates single connection to an SQLite3 database. Its usage is very straightforward: +# +# ``` +# require "sqlite3" +# +# db = SQLite3::Database.new( "data.db" ) +# db.execute("select * from table") do |row| +# p row +# end +# db.close +# ``` +# +# Lower level methods are also provided. class SQLite3::Database + # Creates a new Database object that opens the given file. def initialize(filename) code = LibSQLite3.open_v2(filename, out @db, (LibSQLite3::Flag::READWRITE | LibSQLite3::Flag::CREATE), nil) if code != 0 @@ -7,6 +21,7 @@ class SQLite3::Database @closed = false end + # Creates a new Database object that opens the given file, yields it, and closes it at the end. def self.new(filename) db = new filename begin @@ -16,24 +31,52 @@ class SQLite3::Database end end + # Executes the given SQL statement. If additional parameters are given, they are treated as bind variables, + # and are bound to the placeholders in the query. + # + # Note that if any of the values passed to this are hashes, then the key/value pairs are each bound separately, + # with the key being used as the name of the placeholder to bind the value to. + # + # Returns an `Array(Array(Value))`. def execute(sql, *binds) execute(sql, binds) end + # Executes the given SQL statement. If additional parameters are given, they are treated as bind variables, + # and are bound to the placeholders in the query. + # + # Note that if any of the values passed to this are hashes, then the key/value pairs are each bound separately, + # with the key being used as the name of the placeholder to bind the value to. + # + # Yields one `Array(Value)` for each result. def execute(sql, *binds, &block) execute(sql, binds) do |row| yield row end end + # Executes the given SQL statement. If additional parameters are given, they are treated as bind variables, + # and are bound to the placeholders in the query. + # + # Note that if any of the values passed to this are hashes, then the key/value pairs are each bound separately, + # with the key being used as the name of the placeholder to bind the value to. + # + # Returns an `Array(Array(Value))`. def execute(sql, binds : Enumerable) - rows = [] of Array(SQLite3::ColumnType) + rows = [] of Array(Value) execute(sql, binds) do |row| rows << row end rows end + # Executes the given SQL statement. If additional parameters are given, they are treated as bind variables, + # and are bound to the placeholders in the query. + # + # Note that if any of the values passed to this are hashes, then the key/value pairs are each bound separately, + # with the key being used as the name of the placeholder to bind the value to. + # + # Yields one `Array(Value)` for each result. def execute(sql, binds : Enumerable, &block) query(sql, binds) do |result_set| while result_set.next @@ -42,10 +85,12 @@ class SQLite3::Database end end + # A convenience method that returns the first row of a query result. def get_first_row(sql, *binds) get_first_row(sql, binds) end + # A convenience method that returns the first row of a query result. def get_first_row(sql, binds : Enumerable) query(sql, binds) do |result_set| if result_set.next @@ -56,10 +101,12 @@ class SQLite3::Database end end + # A convenience method that returns the first value of the first row of a query result. def get_first_value(sql, *binds) get_first_value(sql, binds) end + # A convenience method that returns the first value of the first row of a query result. def get_first_value(sql, binds : Enumerable) query(sql, binds) do |result_set| if result_set.next @@ -70,42 +117,53 @@ class SQLite3::Database end end + # Executes a query and gives back a `ResultSet`. def query(sql, *binds) query(sql, binds) end + # Executes a query and yields a `ResultSet` that will be closed at the end of the given block. def query(sql, *binds, &block) query(sql, binds) do |result_set| yield result_set end end + # Executes a query and gives back a `ResultSet`. def query(sql, binds : Enumerable) prepare(sql).execute(binds) end + # Executes a query and yields a `ResultSet` that will be closed at the end of the given block. def query(sql, binds : Enumerable, &block) prepare(sql).execute(binds) do |result_set| yield result_set end end + # Prepares an sql statement. Returns a `Statement`. def prepare(sql) Statement.new(self, sql) end + # Obtains the unique row ID of the last row to be inserted by this Database instance. + # This is an `Int64`. def last_insert_row_id LibSQLite3.last_insert_rowid(self) end + # Quotes the given string, making it safe to use in an SQL statement. + # It replaces all instances of the single-quote character with two single-quote characters. def quote(string) string.gsub('\'', "''") end + # Returns `true` if this database instance has been closed (see `#close`). def closed? @closed end + # Closes this database. def close return if @closed @@ -114,10 +172,12 @@ class SQLite3::Database LibSQLite3.close_v2(@db) end + # :nodoc: def finalize close end + # :nodoc: def to_unsafe @db end diff --git a/src/sqlite3/exception.cr b/src/sqlite3/exception.cr index fae2196..5940144 100644 --- a/src/sqlite3/exception.cr +++ b/src/sqlite3/exception.cr @@ -1,4 +1,6 @@ +# Exception thrown on invalid SQLite3 operations. class SQLite3::Exception < ::Exception + # The internal code associated with the failure. getter code def initialize(db) diff --git a/src/sqlite3/lib_sqlite3.cr b/src/sqlite3/lib_sqlite3.cr index 77fc331..0c8b96e 100644 --- a/src/sqlite3/lib_sqlite3.cr +++ b/src/sqlite3/lib_sqlite3.cr @@ -1,3 +1,5 @@ +require "./type" + @[Link("sqlite3")] lib LibSQLite3 type SQLite3 = Void* @@ -31,14 +33,6 @@ lib LibSQLite3 DONE = 101 end - enum Type - INTEGER = 1 - FLOAT = 2 - BLOB = 4 - NULL = 5 - TEXT = 3 - end - alias Callback = (Void*, Int32, UInt8**, UInt8**) -> Int32 fun open = sqlite3_open_v2(filename : UInt8*, db : SQLite3*) : Int32 @@ -50,7 +44,7 @@ lib LibSQLite3 fun prepare_v2 = sqlite3_prepare_v2(db : SQLite3, zSql : UInt8*, nByte : Int32, ppStmt : Statement*, pzTail : UInt8**) : Int32 fun step = sqlite3_step(stmt : Statement) : Int32 fun column_count = sqlite3_column_count(stmt : Statement) : Int32 - fun column_type = sqlite3_column_type(stmt : Statement, iCol : Int32) : Int32 + fun column_type = sqlite3_column_type(stmt : Statement, iCol : Int32) : SQLite3::Type fun column_int64 = sqlite3_column_int64(stmt : Statement, iCol : Int32) : Int64 fun column_double = sqlite3_column_double(stmt : Statement, iCol : Int32) : Float64 fun column_text = sqlite3_column_text(stmt : Statement, iCol : Int32) : UInt8* diff --git a/src/sqlite3/result_set.cr b/src/sqlite3/result_set.cr index 156ea77..b2f5f1a 100644 --- a/src/sqlite3/result_set.cr +++ b/src/sqlite3/result_set.cr @@ -1,15 +1,48 @@ +# The ResultSet object encapsulates the enumerability of a query’s output. +# It is a simple cursor over the data that the query returns. +# +# Typical usage is: +# +# ``` +# require "sqlite3" +# +# db = SQLite3::Database.new("foo.db") +# stmt = db.prepare("select * from person") +# result_set = stmt.execute +# while result_set.next +# p result_set.to_a +# end +# stmt.close +# db.close +# ``` class SQLite3::ResultSet + # :nodoc: def initialize(@statement) end + # Returns the number of columns. def column_count @statement.column_count end - def [](index) - @statement[index] + # Returns the value of a column by index or name. + def [](index_or_name) + @statement[index_or_name] end + # Returns the types of the columns, an `Array(Type)`. + def types + @statement.types + end + + # Returns the names of the columns, an `Array(String)`. + def columns + @statement.types + end + + # Advances to the next row. Returns `true` if there's a next row, + # `false` otherwise. Must be called at least once to advance to the first + # row. def next case @statement.step when LibSQLite3::Code::ROW @@ -21,11 +54,18 @@ class SQLite3::ResultSet end end + # Closes this result set, closing the associated statement. def close @statement.close end + # Returns `true` if the associated statement is closed. + def closed? + @statement.closed? + end + + # Return the current row's value as an `Array(Value)`. def to_a - Array(ColumnType).new(column_count) { |i| self[i] } + Array(Value).new(column_count) { |i| self[i] } end end diff --git a/src/sqlite3/statement.cr b/src/sqlite3/statement.cr index 1c50614..5b85d92 100644 --- a/src/sqlite3/statement.cr +++ b/src/sqlite3/statement.cr @@ -1,9 +1,12 @@ +# A statement represents a prepared-but-unexecuted SQL query. class SQLite3::Statement + # :nodoc: def initialize(@db, sql) check LibSQLite3.prepare_v2(@db, sql, sql.bytesize + 1, out @stmt, nil) @closed = false end + # :nodoc: def self.new(db, sql) statement = new db, sql begin @@ -13,93 +16,87 @@ class SQLite3::Statement end end + # :nodoc: def step LibSQLite3::Code.new LibSQLite3.step(self) end + # Returns the number of columns in this statement. def column_count LibSQLite3.column_count(self) end - def column_type(index) - LibSQLite3::Type.new LibSQLite3.column_type(self, index.to_i32) + # Returns the `Type` of the column at the given index. + def column_type(index : Int) + LibSQLite3.column_type(self, index.to_i32) end + # Returns the name of the column at the given index. def column_name(index) String.new LibSQLite3.column_name(self, index.to_i32) end - def column_int64(index) - LibSQLite3.column_int64(self, index.to_i32) - end - - def column_double(index) - LibSQLite3.column_double(self, index.to_i32) - end - - def column_text(index) - LibSQLite3.column_text(self, index.to_i32) - end - - def column_blob(index) - LibSQLite3.column_blob(self, index.to_i32) - end - - def column_bytes(index) - LibSQLite3.column_bytes(self, index.to_i32) - end - + # Executes this statement with the given binds and returns a `ResultSet`. def execute(*binds) execute binds end + # Executes this statement with the given binds and yields a `ResultSet` that + # will be closed at the end of the block. def execute(*binds) execute(binds) do |row| yield row end end + # Executes this statement with a single BLOB bind and returns a `ResultSet`. def execute(binds : Slice(UInt8)) reset self[1] = binds ResultSet.new self end + # Executes this statement with the given binds and returns a `ResultSet`. def execute(binds : Enumerable) reset - binds.each_with_index(1) do |bind_value, index| - self[index] = bind_value + # TODO use offset after Crystal 0.6.2 + binds.each_with_index do |bind_value, index| + self[index + 1] = bind_value end ResultSet.new self end + # Executes this statement with the given binds and yields a `ResultSet` that + # will be closed at the end of the block. def execute(binds : Enumerable | Slice(UInt8), &block) result_set = execute(binds) yield result_set close end + # Returns the value of the given column by index (1-based). def [](index : Int) case type = column_type(index) - when LibSQLite3::Type::INTEGER + when Type::INTEGER column_int64(index) - when LibSQLite3::Type::FLOAT + when Type::FLOAT column_double(index) - when LibSQLite3::Type::TEXT + when Type::TEXT String.new(column_text(index)) - when LibSQLite3::Type::BLOB + when Type::BLOB blob = column_blob(index) bytes = column_bytes(index) ptr = Pointer(UInt8).malloc(bytes) ptr.copy_from(blob, bytes) Slice.new(ptr, bytes) - when LibSQLite3::Type::NULL + when Type::NULL nil else raise "Unknown column type: #{type}" end end + # Returns the value of the given column by name. def [](name : String) column_count.times do |i| if column_name(i) == name @@ -109,30 +106,37 @@ class SQLite3::Statement raise "Unknown column: #{name}" end + # Binds the parameter at the given index to an Int. def []=(index : Int, value : Nil) check LibSQLite3.bind_null(self, index.to_i32) end + # Binds the parameter at the given index to an Int32. def []=(index : Int, value : Int32) check LibSQLite3.bind_int(self, index.to_i32, value) end + # Binds the parameter at the given index to an Int64. def []=(index : Int, value : Int64) check LibSQLite3.bind_int64(self, index.to_i32, value) end + # Binds the parameter at the given index to a Float. def []=(index : Int, value : Float) check LibSQLite3.bind_double(self, index.to_i32, value.to_f64) end + # Binds the parameter at the given index to a String. def []=(index : Int, value : String) check LibSQLite3.bind_text(self, index.to_i32, value, value.bytesize, nil) end + # Binds the parameter at the given index to a BLOB. def []=(index : Int, value : Slice(UInt8)) check LibSQLite3.bind_blob(self, index.to_i32, value, value.length, nil) end + # Binds a named parameter, using the `:AAAA` naming scheme for parameters. def []=(name : String | Symbol, value) converted_name = ":#{name}" index = LibSQLite3.bind_parameter_index(self, converted_name) @@ -142,20 +146,31 @@ class SQLite3::Statement self[index] = value end + # Binds a hash to this statement (the `index` is ignored). def []=(index : Int, hash : Hash) hash.each do |key, value| self[key] = value end end + # Returns the column names of this statement. def columns Array.new(column_count) { |i| column_name(i) } end + # Returns an `Array(Type)` of this statement's columns. Note that the statement + # must be executed in order for this to return sensible values, otherwise all types + # will be NULL. + def types + Array.new(column_count) { |i| column_type(i) } + end + + # Reset this statment, allowing to re-execute it with new binds. def reset LibSQLite3.reset(self) end + # Closes this statement. def close raise "Statement already closed" if @closed @closed = true @@ -163,14 +178,36 @@ class SQLite3::Statement check LibSQLite3.finalize(self) end + # Returns `true` if this statement is closed. See `#close`. def closed? @closed end + # :nodoc: def to_unsafe @stmt end + private def column_int64(index) + LibSQLite3.column_int64(self, index.to_i32) + end + + private def column_double(index) + LibSQLite3.column_double(self, index.to_i32) + end + + private def column_text(index) + LibSQLite3.column_text(self, index.to_i32) + end + + private def column_blob(index) + LibSQLite3.column_blob(self, index.to_i32) + end + + private def column_bytes(index) + LibSQLite3.column_bytes(self, index.to_i32) + end + private def check(code) raise Exception.new(@db) unless code == 0 end diff --git a/src/sqlite3/type.cr b/src/sqlite3/type.cr new file mode 100644 index 0000000..4008f39 --- /dev/null +++ b/src/sqlite3/type.cr @@ -0,0 +1,8 @@ +# Each of the possible types of an SQLite3 column. +enum SQLite3::Type + INTEGER = 1 + FLOAT = 2 + BLOB = 4 + NULL = 5 + TEXT = 3 +end diff --git a/src/sqlite3/value.cr b/src/sqlite3/value.cr new file mode 100644 index 0000000..94b134f --- /dev/null +++ b/src/sqlite3/value.cr @@ -0,0 +1,4 @@ +module SQLite3 + # All possible values of each column of a row returned by `Database#execute`. + alias Value = Nil | Int64 | Float64 | String | Slice(UInt8) +end