Compare commits
9 Commits
2.10.0-rc3
...
2.10.0
| Author | SHA1 | Date | |
|---|---|---|---|
| e6dd630caf | |||
| cad14b318a | |||
| 4df1cc17bf | |||
| 884bb331b3 | |||
| ff4a83af47 | |||
| f6f323b307 | |||
| 330280d611 | |||
| 809432cbbd | |||
| d64fee1d15 |
@@ -34,7 +34,11 @@ end
|
||||
ruby_critic_paths = FileList[
|
||||
'speckle_connector/**/*.rb',
|
||||
'speckle_connector.rb',
|
||||
'tests/**/*.rb'] - FileList['_tools/**/*.rb']
|
||||
'tests/**/*.rb'] -
|
||||
FileList[
|
||||
'_tools/**/*.rb',
|
||||
'speckle_connector/src/ext/**/*.rb',
|
||||
]
|
||||
|
||||
# for local
|
||||
RubyCritic::RakeTask.new('rubycritic') do |task|
|
||||
|
||||
+128
-28
@@ -1,6 +1,52 @@
|
||||
#include "Database.h"
|
||||
#include "RubyUtils/RubyUtils.h"
|
||||
#include "ruby.h"
|
||||
#include "sqlite3.h"
|
||||
|
||||
VALUE
|
||||
rbsqlite3_new(VALUE klass, VALUE pathValue)
|
||||
{
|
||||
// Arguments array
|
||||
VALUE argv[1];
|
||||
|
||||
// Convert pathValue to actual path string
|
||||
const char* path;
|
||||
path = StringValuePtr(pathValue);
|
||||
|
||||
SQLite::Database* db = new SQLite::Database(path);
|
||||
|
||||
VALUE obj = Data_Wrap_Struct(klass, NULL, NULL, db);
|
||||
rb_iv_set(obj, "@path", rb_str_new2(path));
|
||||
return obj;
|
||||
}
|
||||
|
||||
VALUE
|
||||
rbsqlite3_table_exist(VALUE klass, VALUE tableNameValue) {
|
||||
// Convert pathValue to actual path string
|
||||
const char* tableName;
|
||||
tableName = StringValuePtr(tableNameValue);
|
||||
|
||||
SQLite::Database* database;
|
||||
Data_Get_Struct(klass, SQLite::Database, database);
|
||||
|
||||
bool val = database->tableExists(tableName);
|
||||
return val ? Qtrue : Qfalse;
|
||||
}
|
||||
|
||||
VALUE
|
||||
rbsqlite3_exec(VALUE klass, VALUE execValue) {
|
||||
VALUE rows = rb_ary_new();
|
||||
|
||||
// Convert pathValue to actual path string
|
||||
const char* query;
|
||||
query = StringValuePtr(execValue);
|
||||
|
||||
SQLite::Database* database;
|
||||
Data_Get_Struct(klass, SQLite::Database, database);
|
||||
|
||||
VALUE val = database->exec(query);
|
||||
return val;
|
||||
}
|
||||
|
||||
VALUE ruby_platform() {
|
||||
return GetRubyInterface(RUBY_PLATFORM);
|
||||
@@ -9,36 +55,91 @@ VALUE ruby_platform() {
|
||||
// Load this module from Ruby using:
|
||||
// require 'Sqlite3'
|
||||
extern "C" {
|
||||
// Proof of concept to test it on Sketchup. Call;
|
||||
// SpeckleConnector::Sqlite.greetings!
|
||||
static VALUE hi_from_c_sqlite3() {
|
||||
char message[] = "hi from c sqlite3!";
|
||||
VALUE str_val = rb_str_new2(message);
|
||||
return str_val;
|
||||
static int hash_callback_function(VALUE callback_ary, int count, char** data, char** columns)
|
||||
{
|
||||
VALUE new_hash = rb_hash_new();
|
||||
int i;
|
||||
|
||||
for (i = 0; i < count; i++) {
|
||||
if (data[i] == NULL) {
|
||||
rb_hash_aset(new_hash, rb_str_new_cstr(columns[i]), Qnil);
|
||||
}
|
||||
else {
|
||||
rb_hash_aset(new_hash, rb_str_new_cstr(columns[i]), rb_str_new_cstr(data[i]));
|
||||
}
|
||||
}
|
||||
|
||||
rb_ary_push(callback_ary, new_hash);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int regular_callback_function(VALUE callback_ary, int count, char** data, char** columns)
|
||||
{
|
||||
VALUE new_ary = rb_ary_new();
|
||||
int i;
|
||||
|
||||
for (i = 0; i < count; i++) {
|
||||
if (data[i] == NULL) {
|
||||
rb_ary_push(new_ary, Qnil);
|
||||
}
|
||||
else {
|
||||
rb_ary_push(new_ary, rb_str_new_cstr(data[i]));
|
||||
}
|
||||
}
|
||||
|
||||
rb_ary_push(callback_ary, new_ary);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Is invoked by calling db.execute_batch2(sql, &block)
|
||||
*
|
||||
* Executes all statements in a given string separated by semicolons.
|
||||
* If a query is made, all values returned are strings
|
||||
* (except for 'NULL' values which return nil),
|
||||
* so the user may parse values with a block.
|
||||
* If no query is made, an empty array will be returned.
|
||||
*/
|
||||
static VALUE rbsqlite3_exec_batch(VALUE self, VALUE sql, VALUE results_as_hash)
|
||||
{
|
||||
SQLite::Database* db;
|
||||
int status;
|
||||
VALUE callback_ary = rb_ary_new();
|
||||
char* errMsg;
|
||||
VALUE errexp;
|
||||
|
||||
Data_Get_Struct(self, SQLite::Database, db);
|
||||
if (!db->getHandle()) \
|
||||
rb_raise(rb_path2class("SQLite3::Exception"), "cannot use a closed database");
|
||||
|
||||
status = sqlite3_exec(db->getHandle(), StringValuePtr(sql), (sqlite3_callback)regular_callback_function, (void*)callback_ary, &errMsg);
|
||||
|
||||
if (status != SQLITE_OK)
|
||||
{
|
||||
errexp = rb_exc_new2(rb_eRuntimeError, errMsg);
|
||||
sqlite3_free(errMsg);
|
||||
rb_exc_raise(errexp);
|
||||
}
|
||||
|
||||
return callback_ary;
|
||||
}
|
||||
|
||||
static void rbsqlite3_free(void* ptr) {
|
||||
delete (SQLite::Database*)ptr;
|
||||
}
|
||||
|
||||
static VALUE rbsqlite3_new(VALUE klass, VALUE pathValue) {
|
||||
VALUE argv[1];
|
||||
const char* path = "C:/Users/sotas/AppData/Roaming/Speckle/Accounts.db";
|
||||
SQLite::Database* database = new SQLite::Database(path);
|
||||
VALUE obj = Data_Wrap_Struct(klass, 0, rbsqlite3_free, database);
|
||||
argv[0] = pathValue;
|
||||
|
||||
rb_obj_call_init(obj, 1, argv);
|
||||
rb_iv_set(obj, "@path", pathValue);
|
||||
return obj;
|
||||
}
|
||||
|
||||
static VALUE rbsqlite3_new2(VALUE klass) {
|
||||
const char* path = "C:/Users/sotas/AppData/Roaming/Speckle/Accounts.db";
|
||||
SQLite::Database* database = new SQLite::Database(path);
|
||||
VALUE obj = Data_Wrap_Struct(klass, 0, rbsqlite3_free, database);
|
||||
rb_obj_call_init(obj, 0, 0);
|
||||
return obj;
|
||||
/* call-seq: db.close
|
||||
*
|
||||
* Closes this database.
|
||||
*/
|
||||
static VALUE rbsqlite3_close(VALUE self)
|
||||
{
|
||||
SQLite::Database* database;
|
||||
Data_Get_Struct(self, SQLite::Database, database);
|
||||
sqlite3_close(database->getHandle());
|
||||
rb_iv_set(self, "-aggregators", Qnil);
|
||||
return self;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,12 +153,11 @@ void Init_sqlite3()
|
||||
VALUE speckle_connector_sqlite3_database = rb_define_class_under(speckle_connector_sqlite3, "Database", rb_cObject);
|
||||
|
||||
rb_define_singleton_method(speckle_connector_sqlite3, "ruby_platform", (ruby_method)ruby_platform, 0);
|
||||
rb_define_singleton_method(speckle_connector_sqlite3, "greetings!", (ruby_method)hi_from_c_sqlite3, 0);
|
||||
|
||||
rb_define_singleton_method(speckle_connector_sqlite3_database, "new", (ruby_method)rbsqlite3_new, 1);
|
||||
rb_define_singleton_method(speckle_connector_sqlite3_database, "new2", (ruby_method)rbsqlite3_new2, 0);
|
||||
|
||||
rb_define_method(speckle_connector_sqlite3_database, "close", (ruby_method)rbsqlite3_free, 0);
|
||||
rb_define_method(speckle_connector_sqlite3_database, "close", (ruby_method)rbsqlite3_close, 0);
|
||||
rb_define_method(speckle_connector_sqlite3_database, "exec", (ruby_method)rbsqlite3_exec_batch, 1);
|
||||
rb_define_method(speckle_connector_sqlite3_database, "table_exist?", (ruby_method)rbsqlite3_table_exist, 1);
|
||||
}
|
||||
|
||||
void Init_sqlite3_20()
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'JSON'
|
||||
require_relative '../ext/sqlite3'
|
||||
require_relative '../constants/path_constants'
|
||||
|
||||
module SpeckleConnector
|
||||
# Accounts to communicate with models on user's account.
|
||||
module Accounts
|
||||
def self.load_accounts
|
||||
require_relative '../ext/sqlite3'
|
||||
|
||||
db_path = SPECKLE_ACCOUNTS_DB_PATH
|
||||
unless File.exist?(db_path)
|
||||
raise(
|
||||
@@ -18,13 +17,8 @@ module SpeckleConnector
|
||||
)
|
||||
end
|
||||
|
||||
db = SQLite3::Database.new(db_path)
|
||||
# FIXME: It's workaround, throws error when queried from database
|
||||
begin
|
||||
rows = db.execute('SELECT * FROM objects')
|
||||
rescue StandardError
|
||||
rows = db.execute('SELECT * FROM objects')
|
||||
end
|
||||
db = Sqlite3::Database.new(db_path)
|
||||
rows = db.exec('SELECT * FROM objects')
|
||||
db.close
|
||||
rows.map { |row| JSON.parse(row[1]) }
|
||||
end
|
||||
|
||||
@@ -18,7 +18,11 @@ module SpeckleConnector
|
||||
# @return [States::State] the new updated state object
|
||||
def update_state(state)
|
||||
converter = Converters::ToNative.new(state.sketchup_state.sketchup_model)
|
||||
# Have side effects on the sketchup model. It effects directly on the entities by adding new objects.
|
||||
start_time = Time.now.to_f
|
||||
converter.traverse_commit_object(@base)
|
||||
elapsed_time = (Time.now.to_f - start_time).round(3)
|
||||
puts "==== Converting to Native executed in #{elapsed_time} sec ===="
|
||||
state.with_add_queue('finishedReceiveInSketchup', @stream_id, [])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,5 +13,6 @@ module SpeckleConnector
|
||||
else
|
||||
raise "Unsupported OS: #{host_os.inspect}"
|
||||
end
|
||||
RUBY_VERSION_NUMBER = RUBY_VERSION.split('.')[0..1].join.to_i
|
||||
end
|
||||
# rubocop:enable Style/Documentation
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# rubocop:disable Metrics/CyclomaticComplexity
|
||||
# rubocop:disable Metrics/PerceivedComplexity
|
||||
|
||||
module SpeckleConnector
|
||||
module Converters
|
||||
# CleanUp is a plugin developed by [Thomas Thomassen](https://github.com/thomthom).
|
||||
module CleanUp
|
||||
# Removes coplanar entities from the given entities.
|
||||
# @param entities [Sketchup::Entities] entities to remove edges between that make entities coplanar.
|
||||
# @note Merging coplanar faces idea originated from [CleanUp](https://github.com/thomthom/cleanup) plugin
|
||||
# which is developed by [Thomas Thomassen](https://github.com/thomthom).
|
||||
def merge_coplanar_faces(entities)
|
||||
edges = []
|
||||
faces = entities.collect { |entity| entity if entity.is_a? Sketchup::Face }.compact
|
||||
faces.each { |face| face.edges.each { |edge| edges << edge } }
|
||||
edges.compact!
|
||||
edges.each { |edge| remove_edge_have_coplanar_faces(edge, false) }
|
||||
end
|
||||
|
||||
# Detect edges to remove by checking following controls respectively;
|
||||
# - Upcoming Sketchup entity is Sketchup::Edge or not.
|
||||
# - Whether edge has 2 face or not.
|
||||
# - Whether faces are duplicated or not.
|
||||
# - Whether edges safe to merge or not.
|
||||
# - Whether faces have same material or not.
|
||||
# - Whether UV texture map is aligned between faces or not.
|
||||
# - Finally, if faces are coplanar by correcting these checks, then removes edge from Sketchup.active_model.
|
||||
# @param edge [Sketchup::Edge] edge to check.
|
||||
# @param ignore_materials [Boolean] whether ignore materials or not.
|
||||
# Returns true if the given edge separating two coplanar faces.
|
||||
# Return false otherwise.
|
||||
def remove_edge_have_coplanar_faces(edge, ignore_materials)
|
||||
return false unless edge.valid? && edge.is_a?(Sketchup::Edge)
|
||||
return false unless edge.faces.size == 2
|
||||
|
||||
face_1, face_2 = edge.faces
|
||||
|
||||
return false if face_duplicate?(face_1, face_2)
|
||||
# Check for troublesome faces which might lead to missing geometry if merged.
|
||||
return false unless edge_safe_to_merge?(edge)
|
||||
|
||||
# Check materials match.
|
||||
unless ignore_materials
|
||||
return false unless (face_1.material == face_2.material) && (face_1.back_material == face_2.back_material)
|
||||
# Verify UV mapping match.
|
||||
if (!face_1.material.nil? || face_1.material.texture.nil?) && !continuous_uv?(face_1, face_2, edge)
|
||||
return false
|
||||
end
|
||||
end
|
||||
# Check faces are coplanar or not.
|
||||
return false unless faces_coplanar?(face_1, face_2)
|
||||
|
||||
edge.erase!
|
||||
true
|
||||
end
|
||||
|
||||
# Determines if two faces are overlapped.
|
||||
def face_duplicate?(face_1, face_2, overlapping: false)
|
||||
return false if face_1 == face_2
|
||||
|
||||
v_1 = face_1.outer_loop.vertices
|
||||
v_2 = face_2.outer_loop.vertices
|
||||
return true if (v_1 - v_2).empty? && (v_2 - v_1).empty?
|
||||
|
||||
if overlapping && (v_2 - v_1).empty?
|
||||
edges = (face_2.outer_loop.edges - face_1.outer_loop.edges)
|
||||
unless edges.empty?
|
||||
point = edges[0].start.position.offset(edges[0].line[1], 0.01)
|
||||
return true if face_1.classify_point(point) <= 4
|
||||
end
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
# Checks the given edge for potential problems if the connected faces would
|
||||
# be merged.
|
||||
def edge_safe_to_merge?(edge)
|
||||
edge.faces.all? { |face| face_safe_to_merge?(face) }
|
||||
end
|
||||
|
||||
# Returns true if the two faces connected by the edge has continuous UV mapping.
|
||||
# UV's are normalized to 0.0..1.0 before comparison.
|
||||
def continuous_uv?(face_1, face_2, edge)
|
||||
tw = Sketchup.create_texture_writer
|
||||
uvh_1 = face_1.get_UVHelper(true, true, tw)
|
||||
uvh_2 = face_2.get_UVHelper(true, true, tw)
|
||||
p_1 = edge.start.position
|
||||
p_2 = edge.end.position
|
||||
uv_equal?(uvh_1.get_front_UVQ(p_1), uvh_2.get_front_UVQ(p_1)) &&
|
||||
uv_equal?(uvh_1.get_front_UVQ(p_2), uvh_2.get_front_UVQ(p_2)) &&
|
||||
uv_equal?(uvh_1.get_back_UVQ(p_1), uvh_2.get_back_UVQ(p_1)) &&
|
||||
uv_equal?(uvh_1.get_back_UVQ(p_2), uvh_2.get_back_UVQ(p_2))
|
||||
end
|
||||
|
||||
# Normalize UV's to 0.0..1.0 and compare them.
|
||||
def uv_equal?(uvq_1, uvq_2)
|
||||
uv_1 = uvq_1.to_a.map { |n| n % 1 }
|
||||
uv_2 = uvq_2.to_a.map { |n| n % 1 }
|
||||
uv_1 == uv_2
|
||||
end
|
||||
|
||||
# Validates that the given face can be merged with other faces without causing
|
||||
# problems.
|
||||
def face_safe_to_merge?(face)
|
||||
stack = face.outer_loop.edges
|
||||
edge = stack.shift
|
||||
direction = edge.line[1]
|
||||
until stack.empty?
|
||||
edge = stack.shift
|
||||
return true unless edge.line[1].parallel?(direction)
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
# Determines if two faces are coplanar.
|
||||
def faces_coplanar?(face_1, face_2)
|
||||
vertices = face_1.vertices + face_2.vertices
|
||||
plane = Geom.fit_plane_to_points(vertices)
|
||||
vertices.all? { |v| v.position.on_plane?(plane) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:enable Metrics/CyclomaticComplexity
|
||||
# rubocop:enable Metrics/PerceivedComplexity
|
||||
@@ -1,21 +1,56 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'converter'
|
||||
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
# rubocop:disable Metrics/PerceivedComplexity
|
||||
# rubocop:disable Metrics/CyclomaticComplexity
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
# rubocop:disable Metrics/ClassLength
|
||||
# rubocop:disable SketchupSuggestions/AddGroup
|
||||
require_relative '../speckle_objects/other/transform'
|
||||
require_relative '../speckle_objects/other/render_material'
|
||||
require_relative '../speckle_objects/geometry/point'
|
||||
require_relative '../speckle_objects/geometry/line'
|
||||
require_relative '../speckle_objects/geometry/mesh'
|
||||
|
||||
module SpeckleConnector
|
||||
module Converters
|
||||
# Converts sketchup entities to speckle objects.
|
||||
class ToNative < Converter
|
||||
# Module aliases
|
||||
GEOMETRY = SpeckleObjects::Geometry
|
||||
OTHER = SpeckleObjects::Other
|
||||
|
||||
# Class aliases
|
||||
POINT = GEOMETRY::Point
|
||||
LINE = GEOMETRY::Line
|
||||
MESH = GEOMETRY::Mesh
|
||||
BLOCK_DEFINITION = OTHER::BlockDefinition
|
||||
BLOCK_INSTANCE = OTHER::BlockInstance
|
||||
|
||||
BASE_OBJECT_PROPS = %w[applicationId id speckle_type totalChildrenCount].freeze
|
||||
CONVERTABLE_SPECKLE_TYPES = %w[
|
||||
Objects.Geometry.Line
|
||||
Objects.Geometry.Polyline
|
||||
Objects.Geometry.Mesh
|
||||
Objects.Geometry.Brep
|
||||
Objects.Other.BlockInstance
|
||||
Objects.Other.BlockDefinition
|
||||
Objects.Other.RenderMaterial
|
||||
].freeze
|
||||
|
||||
def can_convert_to_native(obj)
|
||||
return false unless obj.is_a?(Hash) && obj.key?('speckle_type')
|
||||
|
||||
CONVERTABLE_SPECKLE_TYPES.include?(obj['speckle_type'])
|
||||
end
|
||||
|
||||
def ignored_speckle_type?(obj)
|
||||
['Objects.BuiltElements.Revit.Parameter'].include?(obj['speckle_type'])
|
||||
end
|
||||
|
||||
# Traversal method to create Sketchup objects from upcoming base object.
|
||||
# @param obj [Hash, Array] object might be source base object or it's sub objects, because this method is a
|
||||
# self-caller method means that call itself according to conditions inside of it.
|
||||
# rubocop:disable Metrics/CyclomaticComplexity
|
||||
# rubocop:disable Metrics/PerceivedComplexity
|
||||
def traverse_commit_object(obj)
|
||||
if can_convert_to_native(obj)
|
||||
convert_to_native(obj, Sketchup.active_model.entities)
|
||||
convert_to_native(obj)
|
||||
elsif obj.is_a?(Hash) && obj.key?('speckle_type')
|
||||
return if ignored_speckle_type?(obj)
|
||||
|
||||
@@ -33,359 +68,51 @@ module SpeckleConnector
|
||||
obj.each { |value| traverse_commit_object(value) }
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/CyclomaticComplexity
|
||||
# rubocop:enable Metrics/PerceivedComplexity
|
||||
|
||||
def can_convert_to_native(obj)
|
||||
return false unless obj.is_a?(Hash) && obj.key?('speckle_type')
|
||||
|
||||
[
|
||||
'Objects.Geometry.Line',
|
||||
'Objects.Geometry.Polyline',
|
||||
'Objects.Geometry.Mesh',
|
||||
'Objects.Geometry.Brep',
|
||||
'Objects.Other.BlockInstance',
|
||||
'Objects.Other.BlockDefinition',
|
||||
'Objects.Other.RenderMaterial'
|
||||
].include?(obj['speckle_type'])
|
||||
end
|
||||
|
||||
def ignored_speckle_type?(obj)
|
||||
['Objects.BuiltElements.Revit.Parameter'].include?(obj['speckle_type'])
|
||||
end
|
||||
|
||||
def convert_to_native(obj, entities = Sketchup.active_model.entities)
|
||||
# rubocop:disable Metrics/CyclomaticComplexity
|
||||
def convert_to_native(obj, entities = sketchup_model.entities)
|
||||
return display_value_to_native_component(obj, entities) unless obj['displayValue'].nil?
|
||||
|
||||
convert = method(:convert_to_native)
|
||||
case obj['speckle_type']
|
||||
when 'Objects.Geometry.Line', 'Objects.Geometry.Polyline' then edge_to_native(obj, entities)
|
||||
when 'Objects.Other.BlockInstance' then component_instance_to_native(obj, entities)
|
||||
when 'Objects.Other.BlockDefinition' then component_definition_to_native(obj, entities)
|
||||
when 'Objects.Geometry.Mesh' then mesh_to_native(obj, entities)
|
||||
when 'Objects.Geometry.Brep' then mesh_to_native(obj['displayValue'], entities)
|
||||
when 'Objects.Geometry.Line', 'Objects.Geometry.Polyline' then LINE.to_native(obj, entities)
|
||||
when 'Objects.Other.BlockInstance' then BLOCK_INSTANCE.to_native(sketchup_model, obj, entities, &convert)
|
||||
when 'Objects.Other.BlockDefinition' then BLOCK_DEFINITION.to_native(sketchup_model, obj, entities, &convert)
|
||||
when 'Objects.Geometry.Mesh' then MESH.to_native(sketchup_model, obj, entities)
|
||||
when 'Objects.Geometry.Brep' then MESH.to_native(sketchup_model, obj['displayValue'], entities)
|
||||
end
|
||||
rescue StandardError => e
|
||||
puts("Failed to convert #{obj['speckle_type']} (id: #{obj['id']})")
|
||||
puts(e)
|
||||
nil
|
||||
end
|
||||
|
||||
def length_to_native(length, units = @units)
|
||||
length.__send__(Converters::SKETCHUP_UNIT_STRINGS[units])
|
||||
end
|
||||
|
||||
def edge_to_native(line, entities)
|
||||
if line.key?('value')
|
||||
values = line['value']
|
||||
points = values.each_slice(3).to_a.map { |pt| point_to_native(pt[0], pt[1], pt[2], line['units']) }
|
||||
points.push(points[0]) if line['closed']
|
||||
entities.add_edges(*points)
|
||||
else
|
||||
start_pt = point_to_native(line['start']['x'], line['start']['y'], line['start']['z'], line['units'])
|
||||
end_pt = point_to_native(line['end']['x'], line['end']['y'], line['end']['z'], line['units'])
|
||||
entities.add_edges(start_pt, end_pt)
|
||||
end
|
||||
end
|
||||
|
||||
def edge_to_native_component(line, entities)
|
||||
line_id = line['applicationId'].to_s.empty? ? line['id'] : line['applicationId']
|
||||
definition = component_definition_to_native([line], "def::#{line_id}")
|
||||
find_and_erase_existing_instance(definition, line_id)
|
||||
instance = entities.add_instance(definition, Geom::Transformation.new)
|
||||
instance.name = line_id
|
||||
instance
|
||||
end
|
||||
|
||||
def face_to_native
|
||||
nil
|
||||
end
|
||||
|
||||
def point_to_native(x, y, z, units)
|
||||
Geom::Point3d.new(length_to_native(x, units), length_to_native(y, units), length_to_native(z, units))
|
||||
end
|
||||
|
||||
def point_to_native_array(x, y, z, units)
|
||||
[length_to_native(x, units), length_to_native(y, units), length_to_native(z, units)]
|
||||
end
|
||||
|
||||
# converts a mesh to a native mesh and adds the faces to the given entities collection
|
||||
def mesh_to_native(mesh, entities)
|
||||
_speckle_mesh_to_native_mesh(mesh, entities)
|
||||
end
|
||||
|
||||
def _speckle_mesh_to_native_mesh(mesh, entities)
|
||||
native_mesh = Geom::PolygonMesh.new(mesh['vertices'].count / 3)
|
||||
points = []
|
||||
mesh['vertices'].each_slice(3) do |pt|
|
||||
points.push(point_to_native(pt[0], pt[1], pt[2], mesh['units']))
|
||||
end
|
||||
faces = mesh['faces']
|
||||
while faces.count > 0
|
||||
num_pts = faces.shift
|
||||
# 0 -> 3, 1 -> 4 to preserve backwards compatibility
|
||||
num_pts += 3 if num_pts < 3
|
||||
indices = faces.shift(num_pts)
|
||||
native_mesh.add_polygon(indices.map { |index| points[index] })
|
||||
end
|
||||
entities.add_faces_from_mesh(native_mesh, 4, material_to_native(mesh['renderMaterial']))
|
||||
merge_coplanar_faces(entities)
|
||||
native_mesh
|
||||
end
|
||||
|
||||
# Removes coplanar entities from the given entities.
|
||||
# @param entities [Sketchup::Entities] entities to remove edges between that make entities coplanar.
|
||||
# @note Merging coplanar faces idea originated from [CleanUp](https://github.com/thomthom/cleanup) plugin
|
||||
# which is developed by [Thomas Thomassen](https://github.com/thomthom).
|
||||
def merge_coplanar_faces(entities)
|
||||
edges = []
|
||||
faces = entities.collect { |entity| entity if entity.is_a? Sketchup::Face }.compact
|
||||
faces.each { |face| face.edges.each { |edge| edges << edge } }
|
||||
edges.compact!
|
||||
edges.each { |edge| remove_edge_have_coplanar_faces(edge, false) }
|
||||
end
|
||||
|
||||
# Detect edges to remove by checking following controls respectively;
|
||||
# - Upcoming Sketchup entity is Sketchup::Edge or not.
|
||||
# - Whether edge has 2 face or not.
|
||||
# - Whether faces are duplicated or not.
|
||||
# - Whether edges safe to merge or not.
|
||||
# - Whether faces have same material or not.
|
||||
# - Whether UV texture map is aligned between faces or not.
|
||||
# - Finally, if faces are coplanar by correcting these checks, then removes edge from Sketchup.active_model.
|
||||
# @param edge [Sketchup::Edge] edge to check.
|
||||
# @param ignore_materials [Boolean] whether ignore materials or not.
|
||||
# Returns true if the given edge separating two coplanar faces.
|
||||
# Return false otherwise.
|
||||
def remove_edge_have_coplanar_faces(edge, ignore_materials)
|
||||
return false unless edge.valid? && edge.is_a?(Sketchup::Edge)
|
||||
return false unless edge.faces.size == 2
|
||||
|
||||
face_1, face_2 = edge.faces
|
||||
|
||||
return false if face_duplicate?(face_1, face_2)
|
||||
# Check for troublesome faces which might lead to missing geometry if merged.
|
||||
return false unless edge_safe_to_merge?(edge)
|
||||
|
||||
# Check materials match.
|
||||
unless ignore_materials
|
||||
return false unless (face_1.material == face_2.material) && (face_1.back_material == face_2.back_material)
|
||||
# Verify UV mapping match.
|
||||
if (!face_1.material.nil? || face_1.material.texture.nil?) && !continuous_uv?(face_1, face_2, edge)
|
||||
return false
|
||||
end
|
||||
end
|
||||
# Check faces are coplanar or not.
|
||||
return false unless faces_coplanar?(face_1, face_2)
|
||||
|
||||
edge.erase!
|
||||
true
|
||||
end
|
||||
|
||||
# Determines if two faces are overlapped.
|
||||
def face_duplicate?(face_1, face_2, overlapping: false)
|
||||
return false if face_1 == face_2
|
||||
|
||||
v_1 = face_1.outer_loop.vertices
|
||||
v_2 = face_2.outer_loop.vertices
|
||||
return true if (v_1 - v_2).empty? && (v_2 - v_1).empty?
|
||||
|
||||
if overlapping && (v_2 - v_1).empty?
|
||||
edges = (face_2.outer_loop.edges - face_1.outer_loop.edges)
|
||||
unless edges.empty?
|
||||
point = edges[0].start.position.offset(edges[0].line[1], 0.01)
|
||||
return true if face_1.classify_point(point) <= 4
|
||||
end
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
# Checks the given edge for potential problems if the connected faces would
|
||||
# be merged.
|
||||
def edge_safe_to_merge?(edge)
|
||||
edge.faces.all? { |face| face_safe_to_merge?(face) }
|
||||
end
|
||||
|
||||
# Returns true if the two faces connected by the edge has continuous UV mapping.
|
||||
# UV's are normalized to 0.0..1.0 before comparison.
|
||||
def continuous_uv?(face_1, face_2, edge)
|
||||
tw = Sketchup.create_texture_writer
|
||||
uvh_1 = face_1.get_UVHelper(true, true, tw)
|
||||
uvh_2 = face_2.get_UVHelper(true, true, tw)
|
||||
p_1 = edge.start.position
|
||||
p_2 = edge.end.position
|
||||
uv_equal?(uvh_1.get_front_UVQ(p_1), uvh_2.get_front_UVQ(p_1)) &&
|
||||
uv_equal?(uvh_1.get_front_UVQ(p_2), uvh_2.get_front_UVQ(p_2)) &&
|
||||
uv_equal?(uvh_1.get_back_UVQ(p_1), uvh_2.get_back_UVQ(p_1)) &&
|
||||
uv_equal?(uvh_1.get_back_UVQ(p_2), uvh_2.get_back_UVQ(p_2))
|
||||
end
|
||||
|
||||
# Normalize UV's to 0.0..1.0 and compare them.
|
||||
def uv_equal?(uvq_1, uvq_2)
|
||||
uv_1 = uvq_1.to_a.map { |n| n % 1 }
|
||||
uv_2 = uvq_2.to_a.map { |n| n % 1 }
|
||||
uv_1 == uv_2
|
||||
end
|
||||
|
||||
# Validates that the given face can be merged with other faces without causing
|
||||
# problems.
|
||||
def face_safe_to_merge?(face)
|
||||
stack = face.outer_loop.edges
|
||||
edge = stack.shift
|
||||
direction = edge.line[1]
|
||||
until stack.empty?
|
||||
edge = stack.shift
|
||||
return true unless edge.line[1].parallel?(direction)
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
# Determines if two faces are coplanar.
|
||||
def faces_coplanar?(face_1, face_2)
|
||||
vertices = face_1.vertices + face_2.vertices
|
||||
plane = Geom.fit_plane_to_points(vertices)
|
||||
vertices.all? { |v| v.position.on_plane?(plane) }
|
||||
end
|
||||
|
||||
def _hidden_edges_mesh_to_native_mesh(mesh, entities)
|
||||
native_mesh = Geom::PolygonMesh.new(mesh['vertices'].count / 3)
|
||||
points = []
|
||||
mesh['vertices'].each_slice(3) do |pt|
|
||||
points.push(point_to_native(pt[0], pt[1], pt[2], mesh['units']))
|
||||
end
|
||||
edge_flags = mesh['faceEdgeFlags']
|
||||
faces = mesh['faces']
|
||||
loops = []
|
||||
flags = []
|
||||
while faces.count > 0
|
||||
num_pts = faces.shift
|
||||
# 0 -> 3, 1 -> 4 to preserve backwards compatibility
|
||||
num_pts += 3 if num_pts < 3
|
||||
indices = faces.shift(num_pts)
|
||||
current_edge_flags = edge_flags.shift(num_pts)
|
||||
outer_loop = indices.map { |index| points[index] }
|
||||
if current_edge_flags.include?(true)
|
||||
loops << outer_loop
|
||||
flags << current_edge_flags
|
||||
else
|
||||
native_mesh.add_polygon(outer_loop)
|
||||
end
|
||||
end
|
||||
entities.add_faces_from_mesh(native_mesh, 0, material_to_native(mesh['renderMaterial']))
|
||||
|
||||
loops.each do |l|
|
||||
loop_flags = flags.shift
|
||||
face = entities.add_face(l)
|
||||
face.edges.each_with_index { |edge, index| edge.soft = edge.smooth = loop_flags[index] }
|
||||
end
|
||||
|
||||
native_mesh
|
||||
end
|
||||
# rubocop:enable Metrics/CyclomaticComplexity
|
||||
|
||||
# creates a component definition and instance from a speckle object with a display value
|
||||
def display_value_to_native_component(obj, entities)
|
||||
obj_id = obj['applicationId'].to_s.empty? ? obj['id'] : obj['applicationId']
|
||||
definition = component_definition_to_native(obj['displayValue'], "def::#{obj_id}")
|
||||
definition = BLOCK_DEFINITION.to_native(
|
||||
sketchup_model,
|
||||
obj['displayValue'],
|
||||
"def::#{obj_id}",
|
||||
&method(:convert_to_native)
|
||||
)
|
||||
|
||||
find_and_erase_existing_instance(definition, obj_id)
|
||||
transform = obj['transform'].nil? ? Geom::Transformation.new : transform_to_native(obj['transform'])
|
||||
t_arr = obj['transform']
|
||||
transform = t_arr.nil? ? Geom::Transformation.new : OTHER::Transform.to_native(t_arr, units)
|
||||
instance = entities.add_instance(definition, transform)
|
||||
instance.name = obj_id
|
||||
instance
|
||||
end
|
||||
|
||||
# finds or creates a component definition from the geometry and the given name
|
||||
def component_definition_to_native(geometry, name, application_id = '')
|
||||
definition = Sketchup.active_model.definitions[name]
|
||||
return definition if definition && (definition.name == name || definition.guid == application_id)
|
||||
|
||||
definition&.entities&.clear!
|
||||
definition ||= Sketchup.active_model.definitions.add(name)
|
||||
geometry.each { |obj| convert_to_native(obj, definition.entities) }
|
||||
puts("definition finished: #{name} (#{application_id})")
|
||||
# puts(" entity count: #{definition.entities.count}")
|
||||
definition
|
||||
end
|
||||
|
||||
# takes a component definition and finds and erases the first instance with the matching name
|
||||
# (and optionally the applicationId)
|
||||
def find_and_erase_existing_instance(definition, name, app_id = '')
|
||||
definition.instances.find { |ins| ins.name == name || ins.guid == app_id }&.erase!
|
||||
end
|
||||
|
||||
# creates a component instance from a block
|
||||
def component_instance_to_native(block, entities)
|
||||
# is_group = block.key?("is_sketchup_group") && block["is_sketchup_group"]
|
||||
# something about this conversion is freaking out if nested block geo is a group
|
||||
# so this is set to false always until I can figure this out
|
||||
is_group = false
|
||||
|
||||
definition = component_definition_to_native(
|
||||
block['blockDefinition']['geometry'],
|
||||
block['blockDefinition']['name'],
|
||||
block['blockDefinition']['applicationId']
|
||||
)
|
||||
name = block['name'].nil? || block['name'].empty? ? block['id'] : block['name']
|
||||
transform = transform_to_native(
|
||||
block['transform'].is_a?(Hash) ? block['transform']['value'] : block['transform'],
|
||||
block['units']
|
||||
)
|
||||
instance =
|
||||
if is_group
|
||||
entities.add_group(definition.entities.to_a)
|
||||
else
|
||||
entities.add_instance(definition, transform)
|
||||
end
|
||||
# erase existing instances after creation and before rename because you can't have definitions without instances
|
||||
find_and_erase_existing_instance(definition, name, block['applicationId'])
|
||||
puts("Failed to create instance for speckle block instance #{block['id']}") if instance.nil?
|
||||
instance.transformation = transform if is_group
|
||||
instance.material = material_to_native(block['renderMaterial'])
|
||||
instance.name = name
|
||||
instance
|
||||
end
|
||||
|
||||
def transform_to_native(t_arr, units = @units)
|
||||
Geom::Transformation.new(
|
||||
[
|
||||
t_arr[0],
|
||||
t_arr[4],
|
||||
t_arr[8],
|
||||
t_arr[12],
|
||||
t_arr[1],
|
||||
t_arr[5],
|
||||
t_arr[9],
|
||||
t_arr[13],
|
||||
t_arr[2],
|
||||
t_arr[6],
|
||||
t_arr[10],
|
||||
t_arr[14],
|
||||
length_to_native(t_arr[3], units),
|
||||
length_to_native(t_arr[7], units),
|
||||
length_to_native(t_arr[11], units),
|
||||
t_arr[15]
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
def material_to_native(render_mat)
|
||||
return if render_mat.nil?
|
||||
|
||||
# return material with same name if it exists
|
||||
name = render_mat['name'] || render_mat['id']
|
||||
material = Sketchup.active_model.materials[name]
|
||||
return material if material
|
||||
|
||||
# create a new sketchup material
|
||||
material = Sketchup.active_model.materials.add(name)
|
||||
material.alpha = render_mat['opacity']
|
||||
argb = render_mat['diffuse']
|
||||
material.color = Sketchup::Color.new((argb >> 16) & 255, (argb >> 8) & 255, argb & 255, (argb >> 24) & 255)
|
||||
material
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
# rubocop:enable Metrics/PerceivedComplexity
|
||||
# rubocop:enable Metrics/CyclomaticComplexity
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
# rubocop:enable Metrics/ClassLength
|
||||
# rubocop:enable SketchupSuggestions/AddGroup
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
# support multiple ruby version (fat binaries under windows)
|
||||
begin
|
||||
RUBY_VERSION =~ /(\d+\.\d+)/
|
||||
require "sqlite3/#{$1}/sqlite3_native"
|
||||
rescue LoadError
|
||||
require 'sqlite3/sqlite3_native'
|
||||
end
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'sqlite3/database'
|
||||
require_relative 'sqlite3/version'
|
||||
require_relative '../constants/platform_constants'
|
||||
|
||||
module SQLite3
|
||||
# Was sqlite3 compiled with thread safety on?
|
||||
def self.threadsafe?; threadsafe > 0; end
|
||||
module SpeckleConnector
|
||||
extension = if OPERATING_SYSTEM == OS_WIN
|
||||
'so'
|
||||
else
|
||||
'bundle'
|
||||
end
|
||||
sqlite3_file = "sqlite3_#{RUBY_VERSION_NUMBER}.#{extension}"
|
||||
require_relative(File.join('sqlite3', sqlite3_file))
|
||||
end
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
module SQLite3 ; module Constants
|
||||
|
||||
module TextRep
|
||||
UTF8 = 1
|
||||
UTF16LE = 2
|
||||
UTF16BE = 3
|
||||
UTF16 = 4
|
||||
ANY = 5
|
||||
DETERMINISTIC = 0x800
|
||||
end
|
||||
|
||||
module ColumnType
|
||||
INTEGER = 1
|
||||
FLOAT = 2
|
||||
TEXT = 3
|
||||
BLOB = 4
|
||||
NULL = 5
|
||||
end
|
||||
|
||||
module ErrorCode
|
||||
OK = 0 # Successful result
|
||||
ERROR = 1 # SQL error or missing database
|
||||
INTERNAL = 2 # An internal logic error in SQLite
|
||||
PERM = 3 # Access permission denied
|
||||
ABORT = 4 # Callback routine requested an abort
|
||||
BUSY = 5 # The database file is locked
|
||||
LOCKED = 6 # A table in the database is locked
|
||||
NOMEM = 7 # A malloc() failed
|
||||
READONLY = 8 # Attempt to write a readonly database
|
||||
INTERRUPT = 9 # Operation terminated by sqlite_interrupt()
|
||||
IOERR = 10 # Some kind of disk I/O error occurred
|
||||
CORRUPT = 11 # The database disk image is malformed
|
||||
NOTFOUND = 12 # (Internal Only) Table or record not found
|
||||
FULL = 13 # Insertion failed because database is full
|
||||
CANTOPEN = 14 # Unable to open the database file
|
||||
PROTOCOL = 15 # Database lock protocol error
|
||||
EMPTY = 16 # (Internal Only) Database table is empty
|
||||
SCHEMA = 17 # The database schema changed
|
||||
TOOBIG = 18 # Too much data for one row of a table
|
||||
CONSTRAINT = 19 # Abort due to constraint violation
|
||||
MISMATCH = 20 # Data type mismatch
|
||||
MISUSE = 21 # Library used incorrectly
|
||||
NOLFS = 22 # Uses OS features not supported on host
|
||||
AUTH = 23 # Authorization denied
|
||||
|
||||
ROW = 100 # sqlite_step() has another row ready
|
||||
DONE = 101 # sqlite_step() has finished executing
|
||||
end
|
||||
|
||||
end ; end
|
||||
@@ -1,741 +0,0 @@
|
||||
require_relative 'constants'
|
||||
require_relative 'errors'
|
||||
require_relative 'pragmas'
|
||||
require_relative 'statement'
|
||||
require_relative 'translator'
|
||||
require_relative 'value'
|
||||
|
||||
module SQLite3
|
||||
|
||||
# The Database class encapsulates a single connection to a SQLite3 database.
|
||||
# Its usage is very straightforward:
|
||||
#
|
||||
# require 'sqlite3'
|
||||
#
|
||||
# SQLite3::Database.new( "data.db" ) do |db|
|
||||
# db.execute( "select * from table" ) do |row|
|
||||
# p row
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# It wraps the lower-level methods provides by the selected driver, and
|
||||
# includes the Pragmas module for access to various pragma convenience
|
||||
# methods.
|
||||
#
|
||||
# The Database class provides type translation services as well, by which
|
||||
# the SQLite3 data types (which are all represented as strings) may be
|
||||
# converted into their corresponding types (as defined in the schemas
|
||||
# for their tables). This translation only occurs when querying data from
|
||||
# the database--insertions and updates are all still typeless.
|
||||
#
|
||||
# Furthermore, the Database class has been designed to work well with the
|
||||
# ArrayFields module from Ara Howard. If you require the ArrayFields
|
||||
# module before performing a query, and if you have not enabled results as
|
||||
# hashes, then the results will all be indexible by field name.
|
||||
class Database
|
||||
attr_reader :collations
|
||||
|
||||
include Pragmas
|
||||
|
||||
class << self
|
||||
|
||||
alias :open :new
|
||||
|
||||
# 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. The modified string is returned.
|
||||
def quote( string )
|
||||
string.gsub( /'/, "''" )
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# A boolean that indicates whether rows in result sets should be returned
|
||||
# as hashes or not. By default, rows are returned as arrays.
|
||||
attr_accessor :results_as_hash
|
||||
|
||||
# call-seq: SQLite3::Database.new(file, options = {})
|
||||
#
|
||||
# Create a new Database object that opens the given file. If utf16
|
||||
# is +true+, the filename is interpreted as a UTF-16 encoded string.
|
||||
#
|
||||
# By default, the new database will return result rows as arrays
|
||||
# (#results_as_hash) and has type translation disabled (#type_translation=).
|
||||
|
||||
def initialize file, options = {}, zvfs = nil
|
||||
mode = Constants::Open::READWRITE | Constants::Open::CREATE
|
||||
|
||||
file = file.to_path if file.respond_to? :to_path
|
||||
if file.encoding == ::Encoding::UTF_16LE || file.encoding == ::Encoding::UTF_16BE || options[:utf16]
|
||||
open16 file
|
||||
else
|
||||
# The three primary flag values for sqlite3_open_v2 are:
|
||||
# SQLITE_OPEN_READONLY
|
||||
# SQLITE_OPEN_READWRITE
|
||||
# SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE -- always used for sqlite3_open and sqlite3_open16
|
||||
mode = Constants::Open::READONLY if options[:readonly]
|
||||
|
||||
if options[:readwrite]
|
||||
raise "conflicting options: readonly and readwrite" if options[:readonly]
|
||||
mode = Constants::Open::READWRITE
|
||||
end
|
||||
|
||||
if options[:flags]
|
||||
if options[:readonly] || options[:readwrite]
|
||||
raise "conflicting options: flags with readonly and/or readwrite"
|
||||
end
|
||||
mode = options[:flags]
|
||||
end
|
||||
|
||||
open_v2 file.encode("utf-8"), mode, zvfs
|
||||
|
||||
if options[:strict]
|
||||
disable_quirk_mode
|
||||
end
|
||||
end
|
||||
|
||||
@tracefunc = nil
|
||||
@authorizer = nil
|
||||
@encoding = nil
|
||||
@busy_handler = 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
|
||||
|
||||
if block_given?
|
||||
begin
|
||||
yield self
|
||||
ensure
|
||||
close
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def type_translation= value # :nodoc:
|
||||
warn(<<-eowarn) if $VERBOSE
|
||||
#{caller[0]} is calling SQLite3::Database#type_translation=
|
||||
SQLite3::Database#type_translation= 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
|
||||
# occur, and returning 2 causes the access to be silently denied.
|
||||
def authorizer( &block )
|
||||
self.authorizer = block
|
||||
end
|
||||
|
||||
# Returns a Statement object representing the given SQL. This does not
|
||||
# execute the statement; it merely prepares the statement for execution.
|
||||
#
|
||||
# The Statement can then be executed using Statement#execute.
|
||||
#
|
||||
def prepare sql
|
||||
stmt = SQLite3::Statement.new( self, sql )
|
||||
return stmt unless block_given?
|
||||
|
||||
begin
|
||||
yield stmt
|
||||
ensure
|
||||
stmt.close unless stmt.closed?
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the filename for the database named +db_name+. +db_name+ defaults
|
||||
# to "main". Main return `nil` or an empty string if the database is
|
||||
# temporary or in-memory.
|
||||
def filename db_name = 'main'
|
||||
db_filename db_name
|
||||
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.
|
||||
#
|
||||
# The block is optional. If given, it will be invoked for each row returned
|
||||
# by the query. Otherwise, any results are accumulated into an array and
|
||||
# returned wholesale.
|
||||
#
|
||||
# See also #execute2, #query, and #execute_batch for additional ways of
|
||||
# executing statements.
|
||||
def execute sql, bind_vars = [], *args, &block
|
||||
if bind_vars.nil? || !args.empty?
|
||||
if args.empty?
|
||||
bind_vars = []
|
||||
else
|
||||
bind_vars = [bind_vars] + args
|
||||
end
|
||||
|
||||
warn(<<-eowarn) if $VERBOSE
|
||||
#{caller[0]} is calling SQLite3::Database#execute with nil or multiple bind params
|
||||
without using an array. Please switch to passing bind parameters as an array.
|
||||
Support for bind parameters as *args will be removed in 2.0.0.
|
||||
eowarn
|
||||
end
|
||||
|
||||
prepare( sql ) do |stmt|
|
||||
stmt.bind_params(bind_vars)
|
||||
stmt = ResultSet.new self, stmt
|
||||
|
||||
if block_given?
|
||||
stmt.each do |row|
|
||||
yield row
|
||||
end
|
||||
else
|
||||
stmt.to_a
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Executes the given SQL statement, exactly as with #execute. However, the
|
||||
# first row returned (either via the block, or in the returned array) is
|
||||
# always the names of the columns. Subsequent rows correspond to the data
|
||||
# from the result set.
|
||||
#
|
||||
# Thus, even if the query itself returns no rows, this method will always
|
||||
# return at least one row--the names of the columns.
|
||||
#
|
||||
# See also #execute, #query, and #execute_batch for additional ways of
|
||||
# executing statements.
|
||||
def execute2( sql, *bind_vars )
|
||||
prepare( sql ) do |stmt|
|
||||
result = stmt.execute( *bind_vars )
|
||||
if block_given?
|
||||
yield stmt.columns
|
||||
result.each { |row| yield row }
|
||||
else
|
||||
return result.inject( [ stmt.columns ] ) { |arr,row|
|
||||
arr << row; arr }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Executes all SQL statements in the given string. By contrast, the other
|
||||
# means of executing queries will only execute the first statement in the
|
||||
# string, ignoring all subsequent statements. This will execute each one
|
||||
# in turn. The same bind parameters, if given, will be applied to each
|
||||
# statement.
|
||||
#
|
||||
# This always returns +nil+, making it unsuitable for queries that return
|
||||
# rows.
|
||||
#
|
||||
# See also #execute_batch2 for additional ways of
|
||||
# executing statements.
|
||||
def execute_batch( sql, bind_vars = [], *args )
|
||||
# FIXME: remove this stuff later
|
||||
unless [Array, Hash].include?(bind_vars.class)
|
||||
bind_vars = [bind_vars]
|
||||
warn(<<-eowarn) if $VERBOSE
|
||||
#{caller[0]} is calling SQLite3::Database#execute_batch with bind parameters
|
||||
that are not a list of a hash. Please switch to passing bind parameters as an
|
||||
array or hash. Support for this behavior will be removed in version 2.0.0.
|
||||
eowarn
|
||||
end
|
||||
|
||||
# FIXME: remove this stuff later
|
||||
if bind_vars.nil? || !args.empty?
|
||||
if args.empty?
|
||||
bind_vars = []
|
||||
else
|
||||
bind_vars = [nil] + args
|
||||
end
|
||||
|
||||
warn(<<-eowarn) if $VERBOSE
|
||||
#{caller[0]} is calling SQLite3::Database#execute_batch with nil or multiple bind params
|
||||
without using an array. Please switch to passing bind parameters as an array.
|
||||
Support for this behavior will be removed in version 2.0.0.
|
||||
eowarn
|
||||
end
|
||||
|
||||
sql = sql.strip
|
||||
until sql.empty? do
|
||||
prepare( sql ) do |stmt|
|
||||
unless stmt.closed?
|
||||
# FIXME: this should probably use sqlite3's api for batch execution
|
||||
# This implementation requires stepping over the results.
|
||||
if bind_vars.length == stmt.bind_parameter_count
|
||||
stmt.bind_params(bind_vars)
|
||||
end
|
||||
stmt.step
|
||||
end
|
||||
sql = stmt.remainder.strip
|
||||
end
|
||||
end
|
||||
# FIXME: we should not return `nil` as a success return value
|
||||
nil
|
||||
end
|
||||
|
||||
# Executes all SQL statements in the given string. By contrast, the other
|
||||
# means of executing queries will only execute the first statement in the
|
||||
# string, ignoring all subsequent statements. This will execute each one
|
||||
# in turn. Bind parameters cannot be passed to #execute_batch2.
|
||||
#
|
||||
# If a query is made, all values will be returned as strings.
|
||||
# If no query is made, an empty array will be returned.
|
||||
#
|
||||
# Because all values except for 'NULL' are returned as strings,
|
||||
# a block can be passed to parse the values accordingly.
|
||||
#
|
||||
# See also #execute_batch for additional ways of
|
||||
# executing statements.
|
||||
def execute_batch2(sql, &block)
|
||||
if block_given?
|
||||
result = exec_batch(sql, @results_as_hash)
|
||||
result.map do |val|
|
||||
yield val
|
||||
end
|
||||
else
|
||||
exec_batch(sql, @results_as_hash)
|
||||
end
|
||||
end
|
||||
|
||||
# This is a convenience method for creating a statement, binding
|
||||
# parameters to it, and calling execute:
|
||||
#
|
||||
# result = db.query( "select * from foo where a=?", [5])
|
||||
# # is the same as
|
||||
# result = db.prepare( "select * from foo where a=?" ).execute( 5 )
|
||||
#
|
||||
# You must be sure to call +close+ on the ResultSet instance that is
|
||||
# returned, or you could have problems with locks on the table. If called
|
||||
# with a block, +close+ will be invoked implicitly when the block
|
||||
# terminates.
|
||||
def query( sql, bind_vars = [], *args )
|
||||
|
||||
if bind_vars.nil? || !args.empty?
|
||||
if args.empty?
|
||||
bind_vars = []
|
||||
else
|
||||
bind_vars = [bind_vars] + args
|
||||
end
|
||||
|
||||
warn(<<-eowarn) if $VERBOSE
|
||||
#{caller[0]} is calling SQLite3::Database#query with nil or multiple bind params
|
||||
without using an array. Please switch to passing bind parameters as an array.
|
||||
Support for this will be removed in version 2.0.0.
|
||||
eowarn
|
||||
end
|
||||
|
||||
result = prepare( sql ).execute( bind_vars )
|
||||
if block_given?
|
||||
begin
|
||||
yield result
|
||||
ensure
|
||||
result.close
|
||||
end
|
||||
else
|
||||
return result
|
||||
end
|
||||
end
|
||||
|
||||
# A convenience method for obtaining the first row of a result set, and
|
||||
# discarding all others. It is otherwise identical to #execute.
|
||||
#
|
||||
# See also #get_first_value.
|
||||
def get_first_row( sql, *bind_vars )
|
||||
execute( sql, *bind_vars ).first
|
||||
end
|
||||
|
||||
# A convenience method for obtaining the first value of the first row of a
|
||||
# result set, and discarding all other values and rows. It is otherwise
|
||||
# identical to #execute.
|
||||
#
|
||||
# See also #get_first_row.
|
||||
def get_first_value( sql, *bind_vars )
|
||||
query( sql, bind_vars ) do |rs|
|
||||
if (row = rs.next)
|
||||
return @results_as_hash ? row[rs.columns[0]] : row[0]
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
alias :busy_timeout :busy_timeout=
|
||||
|
||||
# Creates a new function for use in SQL statements. It will be added as
|
||||
# +name+, with the given +arity+. (For variable arity functions, use
|
||||
# -1 for the arity.)
|
||||
#
|
||||
# The block should accept at least one parameter--the FunctionProxy
|
||||
# instance that wraps this function invocation--and any other
|
||||
# arguments it needs (up to its arity).
|
||||
#
|
||||
# The block does not return a value directly. Instead, it will invoke
|
||||
# the FunctionProxy#result= method on the +func+ parameter and
|
||||
# indicate the return value that way.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# db.create_function( "maim", 1 ) do |func, value|
|
||||
# if value.nil?
|
||||
# func.result = nil
|
||||
# else
|
||||
# func.result = value.split(//).sort.join
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# puts db.get_first_value( "select maim(name) from table" )
|
||||
def create_function name, arity, text_rep=Constants::TextRep::UTF8, &block
|
||||
define_function_with_flags(name, text_rep) do |*args|
|
||||
fp = FunctionProxy.new
|
||||
block.call(fp, *args)
|
||||
fp.result
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
# Creates a new aggregate function for use in SQL statements. Aggregate
|
||||
# functions are functions that apply over every row in the result set,
|
||||
# instead of over just a single row. (A very common aggregate function
|
||||
# is the "count" function, for determining the number of rows that match
|
||||
# a query.)
|
||||
#
|
||||
# The new function will be added as +name+, with the given +arity+. (For
|
||||
# variable arity functions, use -1 for the arity.)
|
||||
#
|
||||
# The +step+ parameter must be a proc object that accepts as its first
|
||||
# parameter a FunctionProxy instance (representing the function
|
||||
# invocation), with any subsequent parameters (up to the function's arity).
|
||||
# The +step+ callback will be invoked once for each row of the result set.
|
||||
#
|
||||
# The +finalize+ parameter must be a +proc+ object that accepts only a
|
||||
# single parameter, the FunctionProxy instance representing the current
|
||||
# function invocation. It should invoke FunctionProxy#result= to
|
||||
# store the result of the function.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# db.create_aggregate( "lengths", 1 ) do
|
||||
# step do |func, value|
|
||||
# func[ :total ] ||= 0
|
||||
# func[ :total ] += ( value ? value.length : 0 )
|
||||
# end
|
||||
#
|
||||
# finalize do |func|
|
||||
# func.result = func[ :total ] || 0
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# puts db.get_first_value( "select lengths(name) from table" )
|
||||
#
|
||||
# See also #create_aggregate_handler for a more object-oriented approach to
|
||||
# aggregate functions.
|
||||
def create_aggregate( name, arity, step=nil, finalize=nil,
|
||||
text_rep=Constants::TextRep::ANY, &block )
|
||||
|
||||
proxy = Class.new do
|
||||
def self.step( &block )
|
||||
define_method(:step_with_ctx, &block)
|
||||
end
|
||||
|
||||
def self.finalize( &block )
|
||||
define_method(:finalize_with_ctx, &block)
|
||||
end
|
||||
end
|
||||
|
||||
if block_given?
|
||||
proxy.instance_eval(&block)
|
||||
else
|
||||
proxy.class_eval do
|
||||
define_method(:step_with_ctx, step)
|
||||
define_method(:finalize_with_ctx, finalize)
|
||||
end
|
||||
end
|
||||
|
||||
proxy.class_eval do
|
||||
# class instance variables
|
||||
@name = name
|
||||
@arity = arity
|
||||
|
||||
def self.name
|
||||
@name
|
||||
end
|
||||
|
||||
def self.arity
|
||||
@arity
|
||||
end
|
||||
|
||||
def initialize
|
||||
@ctx = FunctionProxy.new
|
||||
end
|
||||
|
||||
def step( *args )
|
||||
step_with_ctx(@ctx, *args)
|
||||
end
|
||||
|
||||
def finalize
|
||||
finalize_with_ctx(@ctx)
|
||||
@ctx.result
|
||||
end
|
||||
end
|
||||
define_aggregator2(proxy, name)
|
||||
end
|
||||
|
||||
# This is another approach to creating an aggregate function (see
|
||||
# #create_aggregate). Instead of explicitly specifying the name,
|
||||
# callbacks, arity, and type, you specify a factory object
|
||||
# (the "handler") that knows how to obtain all of that information. The
|
||||
# handler should respond to the following messages:
|
||||
#
|
||||
# +arity+:: corresponds to the +arity+ parameter of #create_aggregate. This
|
||||
# message is optional, and if the handler does not respond to it,
|
||||
# the function will have an arity of -1.
|
||||
# +name+:: this is the name of the function. The handler _must_ implement
|
||||
# this message.
|
||||
# +new+:: this must be implemented by the handler. It should return a new
|
||||
# instance of the object that will handle a specific invocation of
|
||||
# the function.
|
||||
#
|
||||
# The handler instance (the object returned by the +new+ message, described
|
||||
# above), must respond to the following messages:
|
||||
#
|
||||
# +step+:: this is the method that will be called for each step of the
|
||||
# aggregate function's evaluation. It should implement the same
|
||||
# signature as the +step+ callback for #create_aggregate.
|
||||
# +finalize+:: this is the method that will be called to finalize the
|
||||
# aggregate function's evaluation. It should implement the
|
||||
# same signature as the +finalize+ callback for
|
||||
# #create_aggregate.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# class LengthsAggregateHandler
|
||||
# def self.arity; 1; end
|
||||
# def self.name; 'lengths'; end
|
||||
#
|
||||
# def initialize
|
||||
# @total = 0
|
||||
# end
|
||||
#
|
||||
# def step( ctx, name )
|
||||
# @total += ( name ? name.length : 0 )
|
||||
# end
|
||||
#
|
||||
# def finalize( ctx )
|
||||
# ctx.result = @total
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# db.create_aggregate_handler( LengthsAggregateHandler )
|
||||
# puts db.get_first_value( "select lengths(name) from A" )
|
||||
def create_aggregate_handler( handler )
|
||||
# This is a compatibility shim so the (basically pointless) FunctionProxy
|
||||
# "ctx" object is passed as first argument to both step() and finalize().
|
||||
# Now its up to the library user whether he prefers to store his
|
||||
# temporaries as instance variables or fields in the FunctionProxy.
|
||||
# The library user still must set the result value with
|
||||
# FunctionProxy.result= as there is no backwards compatible way to
|
||||
# change this.
|
||||
proxy = Class.new(handler) do
|
||||
def initialize
|
||||
super
|
||||
@fp = FunctionProxy.new
|
||||
end
|
||||
|
||||
def step( *args )
|
||||
super(@fp, *args)
|
||||
end
|
||||
|
||||
def finalize
|
||||
super(@fp)
|
||||
@fp.result
|
||||
end
|
||||
end
|
||||
define_aggregator2(proxy, proxy.name)
|
||||
self
|
||||
end
|
||||
|
||||
# Define an aggregate function named +name+ using a object template
|
||||
# object +aggregator+. +aggregator+ must respond to +step+ and +finalize+.
|
||||
# +step+ will be called with row information and +finalize+ must return the
|
||||
# return value for the aggregator function.
|
||||
#
|
||||
# _API Change:_ +aggregator+ must also implement +clone+. The provided
|
||||
# +aggregator+ object will serve as template that is cloned to provide the
|
||||
# individual instances of the aggregate function. Regular ruby objects
|
||||
# already provide a suitable +clone+.
|
||||
# The functions arity is the arity of the +step+ method.
|
||||
def define_aggregator( name, aggregator )
|
||||
# Previously, this has been implemented in C. Now this is just yet
|
||||
# another compatibility shim
|
||||
proxy = Class.new do
|
||||
@template = aggregator
|
||||
@name = name
|
||||
|
||||
def self.template
|
||||
@template
|
||||
end
|
||||
|
||||
def self.name
|
||||
@name
|
||||
end
|
||||
|
||||
def self.arity
|
||||
# this is what sqlite3_obj_method_arity did before
|
||||
@template.method(:step).arity
|
||||
end
|
||||
|
||||
def initialize
|
||||
@klass = self.class.template.clone
|
||||
end
|
||||
|
||||
def step(*args)
|
||||
@klass.step(*args)
|
||||
end
|
||||
|
||||
def finalize
|
||||
@klass.finalize
|
||||
end
|
||||
end
|
||||
define_aggregator2(proxy, name)
|
||||
self
|
||||
end
|
||||
|
||||
# Begins a new transaction. Note that nested transactions are not allowed
|
||||
# by SQLite, so attempting to nest a transaction will result in a runtime
|
||||
# exception.
|
||||
#
|
||||
# The +mode+ parameter may be either <tt>:deferred</tt> (the default),
|
||||
# <tt>:immediate</tt>, or <tt>:exclusive</tt>.
|
||||
#
|
||||
# If a block is given, the database instance is yielded to it, and the
|
||||
# transaction is committed when the block terminates. If the block
|
||||
# raises an exception, a rollback will be performed instead. Note that if
|
||||
# a block is given, #commit and #rollback should never be called
|
||||
# explicitly or you'll get an error when the block terminates.
|
||||
#
|
||||
# If a block is not given, it is the caller's responsibility to end the
|
||||
# transaction explicitly, either by calling #commit, or by calling
|
||||
# #rollback.
|
||||
def transaction( mode = :deferred )
|
||||
execute "begin #{mode.to_s} transaction"
|
||||
|
||||
if block_given?
|
||||
abort = false
|
||||
begin
|
||||
yield self
|
||||
rescue
|
||||
abort = true
|
||||
raise
|
||||
ensure
|
||||
abort and rollback or commit
|
||||
end
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
# Commits the current transaction. If there is no current transaction,
|
||||
# this will cause an error to be raised. This returns +true+, in order
|
||||
# to allow it to be used in idioms like
|
||||
# <tt>abort? and rollback or commit</tt>.
|
||||
def commit
|
||||
execute "commit transaction"
|
||||
true
|
||||
end
|
||||
|
||||
# Rolls the current transaction back. If there is no current transaction,
|
||||
# this will cause an error to be raised. This returns +true+, in order
|
||||
# to allow it to be used in idioms like
|
||||
# <tt>abort? and rollback or commit</tt>.
|
||||
def rollback
|
||||
execute "rollback transaction"
|
||||
true
|
||||
end
|
||||
|
||||
# Returns +true+ if the database has been open in readonly mode
|
||||
# A helper to check before performing any operation
|
||||
def readonly?
|
||||
@readonly
|
||||
end
|
||||
|
||||
# A helper class for dealing with custom functions (see #create_function,
|
||||
# #create_aggregate, and #create_aggregate_handler). It encapsulates the
|
||||
# opaque function object that represents the current invocation. It also
|
||||
# provides more convenient access to the API functions that operate on
|
||||
# the function object.
|
||||
#
|
||||
# This class will almost _always_ be instantiated indirectly, by working
|
||||
# with the create methods mentioned above.
|
||||
class FunctionProxy
|
||||
attr_accessor :result
|
||||
|
||||
# Create a new FunctionProxy that encapsulates the given +func+ object.
|
||||
# If context is non-nil, the functions context will be set to that. If
|
||||
# it is non-nil, it must quack like a Hash. If it is nil, then none of
|
||||
# the context functions will be available.
|
||||
def initialize
|
||||
@result = nil
|
||||
@context = {}
|
||||
end
|
||||
|
||||
# Set the result of the function to the given error message.
|
||||
# The function will then return that error.
|
||||
def set_error( error )
|
||||
@driver.result_error( @func, error.to_s, -1 )
|
||||
end
|
||||
|
||||
# (Only available to aggregate functions.) Returns the number of rows
|
||||
# that the aggregate has processed so far. This will include the current
|
||||
# row, and so will always return at least 1.
|
||||
def count
|
||||
@driver.aggregate_count( @func )
|
||||
end
|
||||
|
||||
# Returns the value with the given key from the context. This is only
|
||||
# available to aggregate functions.
|
||||
def []( key )
|
||||
@context[ key ]
|
||||
end
|
||||
|
||||
# Sets the value with the given key in the context. This is only
|
||||
# available to aggregate functions.
|
||||
def []=( key, value )
|
||||
@context[ 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
|
||||
|
||||
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
|
||||
@@ -1,35 +0,0 @@
|
||||
require_relative 'constants'
|
||||
|
||||
module SQLite3
|
||||
class Exception < ::StandardError
|
||||
# A convenience for accessing the error code for this exception.
|
||||
attr_reader :code
|
||||
end
|
||||
|
||||
class SQLException < Exception; end
|
||||
class InternalException < Exception; end
|
||||
class PermissionException < Exception; end
|
||||
class AbortException < Exception; end
|
||||
class BusyException < Exception; end
|
||||
class LockedException < Exception; end
|
||||
class MemoryException < Exception; end
|
||||
class ReadOnlyException < Exception; end
|
||||
class InterruptException < Exception; end
|
||||
class IOException < Exception; end
|
||||
class CorruptException < Exception; end
|
||||
class NotFoundException < Exception; end
|
||||
class FullException < Exception; end
|
||||
class CantOpenException < Exception; end
|
||||
class ProtocolException < Exception; end
|
||||
class EmptyException < Exception; end
|
||||
class SchemaChangedException < Exception; end
|
||||
class TooBigException < Exception; end
|
||||
class ConstraintException < Exception; end
|
||||
class MismatchException < Exception; end
|
||||
class MisuseException < Exception; end
|
||||
class UnsupportedException < Exception; end
|
||||
class AuthorizationException < Exception; end
|
||||
class FormatException < Exception; end
|
||||
class RangeException < Exception; end
|
||||
class NotADatabaseException < Exception; end
|
||||
end
|
||||
@@ -1,595 +0,0 @@
|
||||
require_relative 'errors'
|
||||
|
||||
module SQLite3
|
||||
|
||||
# This module is intended for inclusion solely by the Database class. It
|
||||
# defines convenience methods for the various pragmas supported by SQLite3.
|
||||
#
|
||||
# For a detailed description of these pragmas, see the SQLite3 documentation
|
||||
# at http://sqlite.org/pragma.html.
|
||||
module Pragmas
|
||||
|
||||
# Returns +true+ or +false+ depending on the value of the named pragma.
|
||||
def get_boolean_pragma( name )
|
||||
get_first_value( "PRAGMA #{name}" ) != "0"
|
||||
end
|
||||
|
||||
# Sets the given pragma to the given boolean value. The value itself
|
||||
# may be +true+ or +false+, or any other commonly used string or
|
||||
# integer that represents truth.
|
||||
def set_boolean_pragma( name, mode )
|
||||
case mode
|
||||
when String
|
||||
case mode.downcase
|
||||
when "on", "yes", "true", "y", "t"; mode = "'ON'"
|
||||
when "off", "no", "false", "n", "f"; mode = "'OFF'"
|
||||
else
|
||||
raise Exception,
|
||||
"unrecognized pragma parameter #{mode.inspect}"
|
||||
end
|
||||
when true, 1
|
||||
mode = "ON"
|
||||
when false, 0, nil
|
||||
mode = "OFF"
|
||||
else
|
||||
raise Exception,
|
||||
"unrecognized pragma parameter #{mode.inspect}"
|
||||
end
|
||||
|
||||
execute( "PRAGMA #{name}=#{mode}" )
|
||||
end
|
||||
|
||||
# Requests the given pragma (and parameters), and if the block is given,
|
||||
# each row of the result set will be yielded to it. Otherwise, the results
|
||||
# are returned as an array.
|
||||
def get_query_pragma( name, *params, &block ) # :yields: row
|
||||
if params.empty?
|
||||
execute( "PRAGMA #{name}", &block )
|
||||
else
|
||||
args = "'" + params.join("','") + "'"
|
||||
execute( "PRAGMA #{name}( #{args} )", &block )
|
||||
end
|
||||
end
|
||||
|
||||
# Return the value of the given pragma.
|
||||
def get_enum_pragma( name )
|
||||
get_first_value( "PRAGMA #{name}" )
|
||||
end
|
||||
|
||||
# Set the value of the given pragma to +mode+. The +mode+ parameter must
|
||||
# conform to one of the values in the given +enum+ array. Each entry in
|
||||
# the array is another array comprised of elements in the enumeration that
|
||||
# have duplicate values. See #synchronous, #default_synchronous,
|
||||
# #temp_store, and #default_temp_store for usage examples.
|
||||
def set_enum_pragma( name, mode, enums )
|
||||
match = enums.find { |p| p.find { |i| i.to_s.downcase == mode.to_s.downcase } }
|
||||
raise Exception,
|
||||
"unrecognized #{name} #{mode.inspect}" unless match
|
||||
execute( "PRAGMA #{name}='#{match.first.upcase}'" )
|
||||
end
|
||||
|
||||
# Returns the value of the given pragma as an integer.
|
||||
def get_int_pragma( name )
|
||||
get_first_value( "PRAGMA #{name}" ).to_i
|
||||
end
|
||||
|
||||
# Set the value of the given pragma to the integer value of the +value+
|
||||
# parameter.
|
||||
def set_int_pragma( name, value )
|
||||
execute( "PRAGMA #{name}=#{value.to_i}" )
|
||||
end
|
||||
|
||||
# The enumeration of valid synchronous modes.
|
||||
SYNCHRONOUS_MODES = [ [ 'full', 2 ], [ 'normal', 1 ], [ 'off', 0 ] ]
|
||||
|
||||
# The enumeration of valid temp store modes.
|
||||
TEMP_STORE_MODES = [ [ 'default', 0 ], [ 'file', 1 ], [ 'memory', 2 ] ]
|
||||
|
||||
# The enumeration of valid auto vacuum modes.
|
||||
AUTO_VACUUM_MODES = [ [ 'none', 0 ], [ 'full', 1 ], [ 'incremental', 2 ] ]
|
||||
|
||||
# The list of valid journaling modes.
|
||||
JOURNAL_MODES = [ [ 'delete' ], [ 'truncate' ], [ 'persist' ], [ 'memory' ],
|
||||
[ 'wal' ], [ 'off' ] ]
|
||||
|
||||
# The list of valid locking modes.
|
||||
LOCKING_MODES = [ [ 'normal' ], [ 'exclusive' ] ]
|
||||
|
||||
# The list of valid encodings.
|
||||
ENCODINGS = [ [ 'utf-8' ], [ 'utf-16' ], [ 'utf-16le' ], [ 'utf-16be ' ] ]
|
||||
|
||||
# The list of valid WAL checkpoints.
|
||||
WAL_CHECKPOINTS = [ [ 'passive' ], [ 'full' ], [ 'restart' ], [ 'truncate' ] ]
|
||||
|
||||
def application_id
|
||||
get_int_pragma "application_id"
|
||||
end
|
||||
|
||||
def application_id=( integer )
|
||||
set_int_pragma "application_id", integer
|
||||
end
|
||||
|
||||
def auto_vacuum
|
||||
get_enum_pragma "auto_vacuum"
|
||||
end
|
||||
|
||||
def auto_vacuum=( mode )
|
||||
set_enum_pragma "auto_vacuum", mode, AUTO_VACUUM_MODES
|
||||
end
|
||||
|
||||
def automatic_index
|
||||
get_boolean_pragma "automatic_index"
|
||||
end
|
||||
|
||||
def automatic_index=( mode )
|
||||
set_boolean_pragma "automatic_index", mode
|
||||
end
|
||||
|
||||
def busy_timeout
|
||||
get_int_pragma "busy_timeout"
|
||||
end
|
||||
|
||||
def busy_timeout=( milliseconds )
|
||||
set_int_pragma "busy_timeout", milliseconds
|
||||
end
|
||||
|
||||
def cache_size
|
||||
get_int_pragma "cache_size"
|
||||
end
|
||||
|
||||
def cache_size=( size )
|
||||
set_int_pragma "cache_size", size
|
||||
end
|
||||
|
||||
def cache_spill
|
||||
get_boolean_pragma "cache_spill"
|
||||
end
|
||||
|
||||
def cache_spill=( mode )
|
||||
set_boolean_pragma "cache_spill", mode
|
||||
end
|
||||
|
||||
def case_sensitive_like=( mode )
|
||||
set_boolean_pragma "case_sensitive_like", mode
|
||||
end
|
||||
|
||||
def cell_size_check
|
||||
get_boolean_pragma "cell_size_check"
|
||||
end
|
||||
|
||||
def cell_size_check=( mode )
|
||||
set_boolean_pragma "cell_size_check", mode
|
||||
end
|
||||
|
||||
def checkpoint_fullfsync
|
||||
get_boolean_pragma "checkpoint_fullfsync"
|
||||
end
|
||||
|
||||
def checkpoint_fullfsync=( mode )
|
||||
set_boolean_pragma "checkpoint_fullfsync", mode
|
||||
end
|
||||
|
||||
def collation_list( &block ) # :yields: row
|
||||
get_query_pragma "collation_list", &block
|
||||
end
|
||||
|
||||
def compile_options( &block ) # :yields: row
|
||||
get_query_pragma "compile_options", &block
|
||||
end
|
||||
|
||||
def count_changes
|
||||
get_boolean_pragma "count_changes"
|
||||
end
|
||||
|
||||
def count_changes=( mode )
|
||||
set_boolean_pragma "count_changes", mode
|
||||
end
|
||||
|
||||
def data_version
|
||||
get_int_pragma "data_version"
|
||||
end
|
||||
|
||||
def database_list( &block ) # :yields: row
|
||||
get_query_pragma "database_list", &block
|
||||
end
|
||||
|
||||
def default_cache_size
|
||||
get_int_pragma "default_cache_size"
|
||||
end
|
||||
|
||||
def default_cache_size=( size )
|
||||
set_int_pragma "default_cache_size", size
|
||||
end
|
||||
|
||||
def default_synchronous
|
||||
get_enum_pragma "default_synchronous"
|
||||
end
|
||||
|
||||
def default_synchronous=( mode )
|
||||
set_enum_pragma "default_synchronous", mode, SYNCHRONOUS_MODES
|
||||
end
|
||||
|
||||
def default_temp_store
|
||||
get_enum_pragma "default_temp_store"
|
||||
end
|
||||
|
||||
def default_temp_store=( mode )
|
||||
set_enum_pragma "default_temp_store", mode, TEMP_STORE_MODES
|
||||
end
|
||||
|
||||
def defer_foreign_keys
|
||||
get_boolean_pragma "defer_foreign_keys"
|
||||
end
|
||||
|
||||
def defer_foreign_keys=( mode )
|
||||
set_boolean_pragma "defer_foreign_keys", mode
|
||||
end
|
||||
|
||||
def encoding
|
||||
get_enum_pragma "encoding"
|
||||
end
|
||||
|
||||
def encoding=( mode )
|
||||
set_enum_pragma "encoding", mode, ENCODINGS
|
||||
end
|
||||
|
||||
def foreign_key_check( *table, &block ) # :yields: row
|
||||
get_query_pragma "foreign_key_check", *table, &block
|
||||
end
|
||||
|
||||
def foreign_key_list( table, &block ) # :yields: row
|
||||
get_query_pragma "foreign_key_list", table, &block
|
||||
end
|
||||
|
||||
def foreign_keys
|
||||
get_boolean_pragma "foreign_keys"
|
||||
end
|
||||
|
||||
def foreign_keys=( mode )
|
||||
set_boolean_pragma "foreign_keys", mode
|
||||
end
|
||||
|
||||
def freelist_count
|
||||
get_int_pragma "freelist_count"
|
||||
end
|
||||
|
||||
def full_column_names
|
||||
get_boolean_pragma "full_column_names"
|
||||
end
|
||||
|
||||
def full_column_names=( mode )
|
||||
set_boolean_pragma "full_column_names", mode
|
||||
end
|
||||
|
||||
def fullfsync
|
||||
get_boolean_pragma "fullfsync"
|
||||
end
|
||||
|
||||
def fullfsync=( mode )
|
||||
set_boolean_pragma "fullfsync", mode
|
||||
end
|
||||
|
||||
def ignore_check_constraints=( mode )
|
||||
set_boolean_pragma "ignore_check_constraints", mode
|
||||
end
|
||||
|
||||
def incremental_vacuum( pages, &block ) # :yields: row
|
||||
get_query_pragma "incremental_vacuum", pages, &block
|
||||
end
|
||||
|
||||
def index_info( index, &block ) # :yields: row
|
||||
get_query_pragma "index_info", index, &block
|
||||
end
|
||||
|
||||
def index_list( table, &block ) # :yields: row
|
||||
get_query_pragma "index_list", table, &block
|
||||
end
|
||||
|
||||
def index_xinfo( index, &block ) # :yields: row
|
||||
get_query_pragma "index_xinfo", index, &block
|
||||
end
|
||||
|
||||
def integrity_check( *num_errors, &block ) # :yields: row
|
||||
get_query_pragma "integrity_check", *num_errors, &block
|
||||
end
|
||||
|
||||
def journal_mode
|
||||
get_enum_pragma "journal_mode"
|
||||
end
|
||||
|
||||
def journal_mode=( mode )
|
||||
set_enum_pragma "journal_mode", mode, JOURNAL_MODES
|
||||
end
|
||||
|
||||
def journal_size_limit
|
||||
get_int_pragma "journal_size_limit"
|
||||
end
|
||||
|
||||
def journal_size_limit=( size )
|
||||
set_int_pragma "journal_size_limit", size
|
||||
end
|
||||
|
||||
def legacy_file_format
|
||||
get_boolean_pragma "legacy_file_format"
|
||||
end
|
||||
|
||||
def legacy_file_format=( mode )
|
||||
set_boolean_pragma "legacy_file_format", mode
|
||||
end
|
||||
|
||||
def locking_mode
|
||||
get_enum_pragma "locking_mode"
|
||||
end
|
||||
|
||||
def locking_mode=( mode )
|
||||
set_enum_pragma "locking_mode", mode, LOCKING_MODES
|
||||
end
|
||||
|
||||
def max_page_count
|
||||
get_int_pragma "max_page_count"
|
||||
end
|
||||
|
||||
def max_page_count=( size )
|
||||
set_int_pragma "max_page_count", size
|
||||
end
|
||||
|
||||
def mmap_size
|
||||
get_int_pragma "mmap_size"
|
||||
end
|
||||
|
||||
def mmap_size=( size )
|
||||
set_int_pragma "mmap_size", size
|
||||
end
|
||||
|
||||
def page_count
|
||||
get_int_pragma "page_count"
|
||||
end
|
||||
|
||||
def page_size
|
||||
get_int_pragma "page_size"
|
||||
end
|
||||
|
||||
def page_size=( size )
|
||||
set_int_pragma "page_size", size
|
||||
end
|
||||
|
||||
def parser_trace=( mode )
|
||||
set_boolean_pragma "parser_trace", mode
|
||||
end
|
||||
|
||||
def query_only
|
||||
get_boolean_pragma "query_only"
|
||||
end
|
||||
|
||||
def query_only=( mode )
|
||||
set_boolean_pragma "query_only", mode
|
||||
end
|
||||
|
||||
def quick_check( *num_errors, &block ) # :yields: row
|
||||
get_query_pragma "quick_check", *num_errors, &block
|
||||
end
|
||||
|
||||
def read_uncommitted
|
||||
get_boolean_pragma "read_uncommitted"
|
||||
end
|
||||
|
||||
def read_uncommitted=( mode )
|
||||
set_boolean_pragma "read_uncommitted", mode
|
||||
end
|
||||
|
||||
def recursive_triggers
|
||||
get_boolean_pragma "recursive_triggers"
|
||||
end
|
||||
|
||||
def recursive_triggers=( mode )
|
||||
set_boolean_pragma "recursive_triggers", mode
|
||||
end
|
||||
|
||||
def reverse_unordered_selects
|
||||
get_boolean_pragma "reverse_unordered_selects"
|
||||
end
|
||||
|
||||
def reverse_unordered_selects=( mode )
|
||||
set_boolean_pragma "reverse_unordered_selects", mode
|
||||
end
|
||||
|
||||
def schema_cookie
|
||||
get_int_pragma "schema_cookie"
|
||||
end
|
||||
|
||||
def schema_cookie=( cookie )
|
||||
set_int_pragma "schema_cookie", cookie
|
||||
end
|
||||
|
||||
def schema_version
|
||||
get_int_pragma "schema_version"
|
||||
end
|
||||
|
||||
def schema_version=( version )
|
||||
set_int_pragma "schema_version", version
|
||||
end
|
||||
|
||||
def secure_delete
|
||||
get_boolean_pragma "secure_delete"
|
||||
end
|
||||
|
||||
def secure_delete=( mode )
|
||||
set_boolean_pragma "secure_delete", mode
|
||||
end
|
||||
|
||||
def short_column_names
|
||||
get_boolean_pragma "short_column_names"
|
||||
end
|
||||
|
||||
def short_column_names=( mode )
|
||||
set_boolean_pragma "short_column_names", mode
|
||||
end
|
||||
|
||||
def shrink_memory
|
||||
execute( "PRAGMA shrink_memory" )
|
||||
end
|
||||
|
||||
def soft_heap_limit
|
||||
get_int_pragma "soft_heap_limit"
|
||||
end
|
||||
|
||||
def soft_heap_limit=( mode )
|
||||
set_int_pragma "soft_heap_limit", mode
|
||||
end
|
||||
|
||||
def stats( &block ) # :yields: row
|
||||
get_query_pragma "stats", &block
|
||||
end
|
||||
|
||||
def synchronous
|
||||
get_enum_pragma "synchronous"
|
||||
end
|
||||
|
||||
def synchronous=( mode )
|
||||
set_enum_pragma "synchronous", mode, SYNCHRONOUS_MODES
|
||||
end
|
||||
|
||||
def temp_store
|
||||
get_enum_pragma "temp_store"
|
||||
end
|
||||
|
||||
def temp_store=( mode )
|
||||
set_enum_pragma "temp_store", mode, TEMP_STORE_MODES
|
||||
end
|
||||
|
||||
def threads
|
||||
get_int_pragma "threads"
|
||||
end
|
||||
|
||||
def threads=( count )
|
||||
set_int_pragma "threads", count
|
||||
end
|
||||
|
||||
def user_cookie
|
||||
get_int_pragma "user_cookie"
|
||||
end
|
||||
|
||||
def user_cookie=( cookie )
|
||||
set_int_pragma "user_cookie", cookie
|
||||
end
|
||||
|
||||
def user_version
|
||||
get_int_pragma "user_version"
|
||||
end
|
||||
|
||||
def user_version=( version )
|
||||
set_int_pragma "user_version", version
|
||||
end
|
||||
|
||||
def vdbe_addoptrace=( mode )
|
||||
set_boolean_pragma "vdbe_addoptrace", mode
|
||||
end
|
||||
|
||||
def vdbe_debug=( mode )
|
||||
set_boolean_pragma "vdbe_debug", mode
|
||||
end
|
||||
|
||||
def vdbe_listing=( mode )
|
||||
set_boolean_pragma "vdbe_listing", mode
|
||||
end
|
||||
|
||||
def vdbe_trace
|
||||
get_boolean_pragma "vdbe_trace"
|
||||
end
|
||||
|
||||
def vdbe_trace=( mode )
|
||||
set_boolean_pragma "vdbe_trace", mode
|
||||
end
|
||||
|
||||
def wal_autocheckpoint
|
||||
get_int_pragma "wal_autocheckpoint"
|
||||
end
|
||||
|
||||
def wal_autocheckpoint=( mode )
|
||||
set_int_pragma "wal_autocheckpoint", mode
|
||||
end
|
||||
|
||||
def wal_checkpoint
|
||||
get_enum_pragma "wal_checkpoint"
|
||||
end
|
||||
|
||||
def wal_checkpoint=( mode )
|
||||
set_enum_pragma "wal_checkpoint", mode, WAL_CHECKPOINTS
|
||||
end
|
||||
|
||||
def writable_schema=( mode )
|
||||
set_boolean_pragma "writable_schema", mode
|
||||
end
|
||||
|
||||
###
|
||||
# Returns information about +table+. Yields each row of table information
|
||||
# if a block is provided.
|
||||
def table_info table
|
||||
stmt = prepare "PRAGMA table_info(#{table})"
|
||||
columns = stmt.columns
|
||||
|
||||
needs_tweak_default =
|
||||
version_compare(SQLite3.libversion.to_s, "3.3.7") > 0
|
||||
|
||||
result = [] unless block_given?
|
||||
stmt.each do |row|
|
||||
new_row = Hash[columns.zip(row)]
|
||||
|
||||
# FIXME: This should be removed but is required for older versions
|
||||
# of rails
|
||||
if(Object.const_defined?(:ActiveRecord))
|
||||
new_row['notnull'] = new_row['notnull'].to_s
|
||||
end
|
||||
|
||||
tweak_default(new_row) if needs_tweak_default
|
||||
|
||||
# Ensure the type value is downcased. On Mac and Windows
|
||||
# platforms this value is now being returned as all upper
|
||||
# case.
|
||||
if new_row['type']
|
||||
new_row['type'] = new_row['type'].downcase
|
||||
end
|
||||
|
||||
if block_given?
|
||||
yield new_row
|
||||
else
|
||||
result << new_row
|
||||
end
|
||||
end
|
||||
stmt.close
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Compares two version strings
|
||||
def version_compare(v1, v2)
|
||||
v1 = v1.split(".").map { |i| i.to_i }
|
||||
v2 = v2.split(".").map { |i| i.to_i }
|
||||
parts = [v1.length, v2.length].max
|
||||
v1.push 0 while v1.length < parts
|
||||
v2.push 0 while v2.length < parts
|
||||
v1.zip(v2).each do |a,b|
|
||||
return -1 if a < b
|
||||
return 1 if a > b
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
# Since SQLite 3.3.8, the table_info pragma has returned the default
|
||||
# value of the row as a quoted SQL value. This method essentially
|
||||
# unquotes those values.
|
||||
def tweak_default(hash)
|
||||
case hash["dflt_value"]
|
||||
when /^null$/i
|
||||
hash["dflt_value"] = nil
|
||||
when /^'(.*)'$/m
|
||||
hash["dflt_value"] = $1.gsub(/''/, "'")
|
||||
when /^"(.*)"$/m
|
||||
hash["dflt_value"] = $1.gsub(/""/, '"')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,187 +0,0 @@
|
||||
require_relative 'constants'
|
||||
require_relative 'errors'
|
||||
|
||||
module SQLite3
|
||||
|
||||
# The ResultSet object encapsulates the enumerability of a query's output.
|
||||
# It is a simple cursor over the data that the query returns. It will
|
||||
# very rarely (if ever) be instantiated directly. Instead, clients should
|
||||
# obtain a ResultSet instance via Statement#execute.
|
||||
class ResultSet
|
||||
include Enumerable
|
||||
|
||||
class ArrayWithTypes < Array # :nodoc:
|
||||
attr_accessor :types
|
||||
end
|
||||
|
||||
class ArrayWithTypesAndFields < Array # :nodoc:
|
||||
attr_writer :types
|
||||
attr_writer :fields
|
||||
|
||||
def types
|
||||
warn(<<-eowarn) if $VERBOSE
|
||||
#{caller[0]} is calling #{self.class}#types. This method will be removed in
|
||||
sqlite3 version 2.0.0, please call the `types` method on the SQLite3::ResultSet
|
||||
object that created this object
|
||||
eowarn
|
||||
@types
|
||||
end
|
||||
|
||||
def fields
|
||||
warn(<<-eowarn) if $VERBOSE
|
||||
#{caller[0]} is calling #{self.class}#fields. This method will be removed in
|
||||
sqlite3 version 2.0.0, please call the `columns` method on the SQLite3::ResultSet
|
||||
object that created this object
|
||||
eowarn
|
||||
@fields
|
||||
end
|
||||
end
|
||||
|
||||
# The class of which we return an object in case we want a Hash as
|
||||
# result.
|
||||
class HashWithTypesAndFields < Hash # :nodoc:
|
||||
attr_writer :types
|
||||
attr_writer :fields
|
||||
|
||||
def types
|
||||
warn(<<-eowarn) if $VERBOSE
|
||||
#{caller[0]} is calling #{self.class}#types. This method will be removed in
|
||||
sqlite3 version 2.0.0, please call the `types` method on the SQLite3::ResultSet
|
||||
object that created this object
|
||||
eowarn
|
||||
@types
|
||||
end
|
||||
|
||||
def fields
|
||||
warn(<<-eowarn) if $VERBOSE
|
||||
#{caller[0]} is calling #{self.class}#fields. This method will be removed in
|
||||
sqlite3 version 2.0.0, please call the `columns` method on the SQLite3::ResultSet
|
||||
object that created this object
|
||||
eowarn
|
||||
@fields
|
||||
end
|
||||
|
||||
def [] key
|
||||
key = fields[key] if key.is_a? Numeric
|
||||
super key
|
||||
end
|
||||
end
|
||||
|
||||
# Create a new ResultSet attached to the given database, using the
|
||||
# given sql text.
|
||||
def initialize db, stmt
|
||||
@db = db
|
||||
@stmt = stmt
|
||||
end
|
||||
|
||||
# Reset the cursor, so that a result set which has reached end-of-file
|
||||
# can be rewound and reiterated.
|
||||
def reset( *bind_params )
|
||||
@stmt.reset!
|
||||
@stmt.bind_params( *bind_params )
|
||||
@eof = false
|
||||
end
|
||||
|
||||
# Query whether the cursor has reached the end of the result set or not.
|
||||
def eof?
|
||||
@stmt.done?
|
||||
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.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# For arrays, the column names are accessible via the +fields+ property,
|
||||
# and the column types are accessible via the +types+ property.
|
||||
#
|
||||
# For hashes, the column names are the keys of the hash, and the column
|
||||
# types are accessible via the +types+ property.
|
||||
def next
|
||||
if @db.results_as_hash
|
||||
return next_hash
|
||||
end
|
||||
|
||||
row = @stmt.step
|
||||
return nil if @stmt.done?
|
||||
|
||||
row = @db.translate_from_db @stmt.types, 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.
|
||||
row = 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.
|
||||
row = ArrayWithTypesAndFields.new(row)
|
||||
end
|
||||
|
||||
row.fields = @stmt.columns
|
||||
row.types = @stmt.types
|
||||
row
|
||||
end
|
||||
|
||||
# Required by the Enumerable mixin. Provides an internal iterator over the
|
||||
# rows of the result set.
|
||||
def each
|
||||
while node = self.next
|
||||
yield node
|
||||
end
|
||||
end
|
||||
|
||||
# Provides an internal iterator over the rows of the result set where
|
||||
# each row is yielded as a hash.
|
||||
def each_hash
|
||||
while node = next_hash
|
||||
yield node
|
||||
end
|
||||
end
|
||||
|
||||
# Closes the statement that spawned this result set.
|
||||
# <em>Use with caution!</em> Closing a result set will automatically
|
||||
# close any other result sets that were spawned from the same statement.
|
||||
def close
|
||||
@stmt.close
|
||||
end
|
||||
|
||||
# Queries whether the underlying statement has been closed or not.
|
||||
def closed?
|
||||
@stmt.closed?
|
||||
end
|
||||
|
||||
# Returns the types of the columns returned by this result set.
|
||||
def types
|
||||
@stmt.types
|
||||
end
|
||||
|
||||
# Returns the names of the columns returned by this result set.
|
||||
def columns
|
||||
@stmt.columns
|
||||
end
|
||||
|
||||
# Return the next row as a hash
|
||||
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]
|
||||
|
||||
# FIXME: these methods are deprecated for version 2.0, so we can remove
|
||||
# this code in 2.0
|
||||
row.fields = @stmt.columns
|
||||
row.types = @stmt.types
|
||||
row
|
||||
end
|
||||
end
|
||||
end
|
||||
Binary file not shown.
@@ -1,145 +0,0 @@
|
||||
require_relative 'errors'
|
||||
require_relative 'resultset'
|
||||
|
||||
class String
|
||||
def to_blob
|
||||
SQLite3::Blob.new( self )
|
||||
end
|
||||
end
|
||||
|
||||
module SQLite3
|
||||
# A statement represents a prepared-but-unexecuted SQL query. It will rarely
|
||||
# (if ever) be instantiated directly by a client, and is most often obtained
|
||||
# via the Database#prepare method.
|
||||
class Statement
|
||||
include Enumerable
|
||||
|
||||
# This is any text that followed the first valid SQL statement in the text
|
||||
# with which the statement was initialized. If there was no trailing text,
|
||||
# this will be the empty string.
|
||||
attr_reader :remainder
|
||||
|
||||
# Binds the given variables to the corresponding placeholders in the SQL
|
||||
# text.
|
||||
#
|
||||
# See Database#execute for a description of the valid placeholder
|
||||
# syntaxes.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# stmt = db.prepare( "select * from table where a=? and b=?" )
|
||||
# stmt.bind_params( 15, "hello" )
|
||||
#
|
||||
# See also #execute, #bind_param, Statement#bind_param, and
|
||||
# Statement#bind_params.
|
||||
def bind_params( *bind_vars )
|
||||
index = 1
|
||||
bind_vars.flatten.each do |var|
|
||||
if Hash === var
|
||||
var.each { |key, val| bind_param key, val }
|
||||
else
|
||||
bind_param index, var
|
||||
index += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Execute the statement. This creates a new ResultSet object for the
|
||||
# statement's virtual machine. If a block was given, the new ResultSet will
|
||||
# be yielded to it; otherwise, the ResultSet will be returned.
|
||||
#
|
||||
# Any parameters will be bound to the statement using #bind_params.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# stmt = db.prepare( "select * from table" )
|
||||
# stmt.execute do |result|
|
||||
# ...
|
||||
# end
|
||||
#
|
||||
# See also #bind_params, #execute!.
|
||||
def execute( *bind_vars )
|
||||
reset! if active? || done?
|
||||
|
||||
bind_params(*bind_vars) unless bind_vars.empty?
|
||||
@results = ResultSet.new(@connection, self)
|
||||
|
||||
step if 0 == column_count
|
||||
|
||||
yield @results if block_given?
|
||||
@results
|
||||
end
|
||||
|
||||
# Execute the statement. If no block was given, this returns an array of
|
||||
# rows returned by executing the statement. Otherwise, each row will be
|
||||
# yielded to the block.
|
||||
#
|
||||
# Any parameters will be bound to the statement using #bind_params.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# stmt = db.prepare( "select * from table" )
|
||||
# stmt.execute! do |row|
|
||||
# ...
|
||||
# end
|
||||
#
|
||||
# See also #bind_params, #execute.
|
||||
def execute!( *bind_vars, &block )
|
||||
execute(*bind_vars)
|
||||
block_given? ? each(&block) : to_a
|
||||
end
|
||||
|
||||
# Returns true if the statement is currently active, meaning it has an
|
||||
# open result set.
|
||||
def active?
|
||||
!done?
|
||||
end
|
||||
|
||||
# Return an array of the column names for this statement. Note that this
|
||||
# may execute the statement in order to obtain the metadata; this makes it
|
||||
# a (potentially) expensive operation.
|
||||
def columns
|
||||
get_metadata unless @columns
|
||||
return @columns
|
||||
end
|
||||
|
||||
def each
|
||||
loop do
|
||||
val = step
|
||||
break self if done?
|
||||
yield val
|
||||
end
|
||||
end
|
||||
|
||||
# Return an array of the data types for each column in this statement. Note
|
||||
# that this may execute the statement in order to obtain the metadata; this
|
||||
# makes it a (potentially) expensive operation.
|
||||
def types
|
||||
must_be_open!
|
||||
get_metadata unless @types
|
||||
@types
|
||||
end
|
||||
|
||||
# Performs a sanity check to ensure that the statement is not
|
||||
# closed. If it is, an exception is raised.
|
||||
def must_be_open! # :nodoc:
|
||||
if closed?
|
||||
raise SQLite3::Exception, "cannot use a closed statement"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
# A convenience method for obtaining the metadata about the query. Note
|
||||
# that this will actually execute the SQL, which means it can be a
|
||||
# (potentially) expensive operation.
|
||||
def get_metadata
|
||||
@columns = Array.new(column_count) do |column|
|
||||
column_name column
|
||||
end
|
||||
@types = Array.new(column_count) do |column|
|
||||
val = column_decltype(column)
|
||||
val.nil? ? nil : val.downcase
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,118 +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[0]} is calling `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 type =~ /\(\s*1\s*\)/
|
||||
value.to_i == 1
|
||||
else
|
||||
value.to_i
|
||||
end
|
||||
end
|
||||
end
|
||||
private :register_default_translators
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,57 +0,0 @@
|
||||
require_relative 'constants'
|
||||
|
||||
module SQLite3
|
||||
|
||||
class Value
|
||||
attr_reader :handle
|
||||
|
||||
def initialize( db, handle )
|
||||
@driver = db.driver
|
||||
@handle = handle
|
||||
end
|
||||
|
||||
def null?
|
||||
type == :null
|
||||
end
|
||||
|
||||
def to_blob
|
||||
@driver.value_blob( @handle )
|
||||
end
|
||||
|
||||
def length( utf16=false )
|
||||
if utf16
|
||||
@driver.value_bytes16( @handle )
|
||||
else
|
||||
@driver.value_bytes( @handle )
|
||||
end
|
||||
end
|
||||
|
||||
def to_f
|
||||
@driver.value_double( @handle )
|
||||
end
|
||||
|
||||
def to_i
|
||||
@driver.value_int( @handle )
|
||||
end
|
||||
|
||||
def to_int64
|
||||
@driver.value_int64( @handle )
|
||||
end
|
||||
|
||||
def to_s( utf16=false )
|
||||
@driver.value_text( @handle, utf16 )
|
||||
end
|
||||
|
||||
def type
|
||||
case @driver.value_type( @handle )
|
||||
when Constants::ColumnType::INTEGER then :int
|
||||
when Constants::ColumnType::FLOAT then :float
|
||||
when Constants::ColumnType::TEXT then :text
|
||||
when Constants::ColumnType::BLOB then :blob
|
||||
when Constants::ColumnType::NULL then :null
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,23 +0,0 @@
|
||||
module SQLite3
|
||||
|
||||
VERSION = "1.5.3"
|
||||
|
||||
module VersionProxy
|
||||
MAJOR = 1
|
||||
MINOR = 5
|
||||
TINY = 3
|
||||
BUILD = nil
|
||||
|
||||
STRING = [ MAJOR, MINOR, TINY, BUILD ].compact.join( "." )
|
||||
|
||||
VERSION = ::SQLite3::VERSION
|
||||
end
|
||||
|
||||
def self.const_missing(name)
|
||||
return super unless name == :Version
|
||||
warn(<<-eowarn) if $VERBOSE
|
||||
#{caller[0]}: SQLite::Version will be removed in sqlite3-ruby version 2.0.0
|
||||
eowarn
|
||||
VersionProxy
|
||||
end
|
||||
end
|
||||
@@ -7,6 +7,10 @@ module SpeckleConnector
|
||||
def self.length_to_speckle(length, units)
|
||||
length.__send__("to_#{SpeckleConnector::Converters::SKETCHUP_UNIT_STRINGS[units]}")
|
||||
end
|
||||
|
||||
def self.length_to_native(length, units)
|
||||
length.__send__(SpeckleConnector::Converters::SKETCHUP_UNIT_STRINGS[units])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -37,6 +37,21 @@ module SpeckleConnector
|
||||
)
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
def self.to_native(line, entities)
|
||||
if line.key?('value')
|
||||
values = line['value']
|
||||
points = values.each_slice(3).to_a.map { |pt| Point.to_native(pt[0], pt[1], pt[2], line['units']) }
|
||||
points.push(points[0]) if line['closed']
|
||||
entities.add_edges(*points)
|
||||
else
|
||||
start_pt = Point.to_native(line['start']['x'], line['start']['y'], line['start']['z'], line['units'])
|
||||
end_pt = Point.to_native(line['end']['x'], line['end']['y'], line['end']['z'], line['units'])
|
||||
entities.add_edges(start_pt, end_pt)
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
|
||||
def attribute_types
|
||||
ATTRIBUTES
|
||||
end
|
||||
|
||||
@@ -3,10 +3,14 @@
|
||||
require_relative '../geometry/bounding_box'
|
||||
require_relative '../other/render_material'
|
||||
require_relative '../../typescript/typescript_object'
|
||||
require_relative '../../convertors/clean_up'
|
||||
|
||||
module SpeckleConnector
|
||||
module SpeckleObjects
|
||||
# Geometry objects in the Speckleverse.
|
||||
module Geometry
|
||||
include Converters::CleanUp
|
||||
|
||||
# Mesh object definition for Speckle.
|
||||
class Mesh < Typescript::TypescriptObject
|
||||
SPECKLE_TYPE = 'Objects.Geometry.Mesh'
|
||||
@@ -20,6 +24,26 @@ module SpeckleConnector
|
||||
'@(31250)faceEdgeFlags': Array
|
||||
}.freeze
|
||||
|
||||
def self.to_native(sketchup_model, mesh, entities)
|
||||
native_mesh = Geom::PolygonMesh.new(mesh['vertices'].count / 3)
|
||||
points = []
|
||||
mesh['vertices'].each_slice(3) do |pt|
|
||||
points.push(Point.to_native(pt[0], pt[1], pt[2], mesh['units']))
|
||||
end
|
||||
faces = mesh['faces']
|
||||
while faces.count > 0
|
||||
num_pts = faces.shift
|
||||
# 0 -> 3, 1 -> 4 to preserve backwards compatibility
|
||||
num_pts += 3 if num_pts < 3
|
||||
indices = faces.shift(num_pts)
|
||||
native_mesh.add_polygon(indices.map { |index| points[index] })
|
||||
end
|
||||
material = Other::RenderMaterial.to_native(sketchup_model, mesh['renderMaterial'])
|
||||
entities.add_faces_from_mesh(native_mesh, 4, material)
|
||||
merge_coplanar_faces(entities)
|
||||
native_mesh
|
||||
end
|
||||
|
||||
# @param face [Sketchup::Face] face to convert mesh
|
||||
def self.from_face(face, units)
|
||||
mesh = face.loops.count > 1 ? face.mesh : nil
|
||||
|
||||
@@ -37,6 +37,14 @@ module SpeckleConnector
|
||||
)
|
||||
end
|
||||
|
||||
def self.to_native(x, y, z, units)
|
||||
Geom::Point3d.new(
|
||||
Geometry.length_to_native(x, units),
|
||||
Geometry.length_to_native(y, units),
|
||||
Geometry.length_to_native(z, units)
|
||||
)
|
||||
end
|
||||
|
||||
def attribute_types
|
||||
ATTRIBUTES
|
||||
end
|
||||
|
||||
@@ -50,6 +50,21 @@ module SpeckleConnector
|
||||
)
|
||||
end
|
||||
|
||||
# finds or creates a component definition from the geometry and the given name
|
||||
# rubocop:disable Metrics/CyclomaticComplexity
|
||||
def self.to_native(sketchup_model, geometry, name, application_id = '', &convert)
|
||||
definition = sketchup_model.definitions[name]
|
||||
return definition if definition && (definition.name == name || definition.guid == application_id)
|
||||
|
||||
definition&.entities&.clear!
|
||||
definition ||= sketchup_model.definitions.add(name)
|
||||
geometry.each { |obj| convert.call(obj, definition.entities) }
|
||||
puts("definition finished: #{name} (#{application_id})")
|
||||
# puts(" entity count: #{definition.entities.count}")
|
||||
definition
|
||||
end
|
||||
# rubocop:enable Metrics/CyclomaticComplexity
|
||||
|
||||
def self.group_mesh_to_speckle(definition, units, definitions)
|
||||
# {material_id => Mesh}
|
||||
mat_groups = {}
|
||||
|
||||
@@ -54,6 +54,46 @@ module SpeckleConnector
|
||||
)
|
||||
end
|
||||
|
||||
# Creates a component instance from a block
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
def self.to_native(sketchup_model, block, entities, &convert)
|
||||
# is_group = block.key?("is_sketchup_group") && block["is_sketchup_group"]
|
||||
# something about this conversion is freaking out if nested block geo is a group
|
||||
# so this is set to false always until I can figure this out
|
||||
is_group = false
|
||||
definition = BLOCK_DEFINITION.to_native(
|
||||
sketchup_model, block['blockDefinition']['geometry'], block['blockDefinition']['name'],
|
||||
block['blockDefinition']['applicationId'], convert
|
||||
)
|
||||
|
||||
name = block['name'].nil? || block['name'].empty? ? block['id'] : block['name']
|
||||
t_arr = block['transform'].is_a?(Hash) ? block['transform']['value'] : block['transform']
|
||||
transform = Other::Transform.to_native(t_arr, block['units'])
|
||||
instance =
|
||||
if is_group
|
||||
# rubocop:disable SketchupSuggestions/AddGroup
|
||||
entities.add_group(definition.entities.to_a)
|
||||
# rubocop:enable SketchupSuggestions/AddGroup
|
||||
else
|
||||
entities.add_instance(definition, transform)
|
||||
end
|
||||
# erase existing instances after creation and before rename because you can't have definitions
|
||||
# without instances
|
||||
find_and_erase_existing_instance(definition, name, block['applicationId'])
|
||||
puts("Failed to create instance for speckle block instance #{block['id']}") if instance.nil?
|
||||
instance.transformation = transform if is_group
|
||||
instance.material = Other::RenderMaterial.to_native(sketchup_model, block['renderMaterial'])
|
||||
instance.name = name
|
||||
instance
|
||||
end
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
|
||||
# takes a component definition and finds and erases the first instance with the matching name
|
||||
# (and optionally the applicationId)
|
||||
def self.find_and_erase_existing_instance(definition, name, app_id = '')
|
||||
definition.instances.find { |ins| ins.name == name || ins.guid == app_id }&.erase!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def attribute_types
|
||||
|
||||
@@ -31,6 +31,22 @@ module SpeckleConnector
|
||||
)
|
||||
end
|
||||
|
||||
def self.to_native(sketchup_model, render_material)
|
||||
return if render_material.nil?
|
||||
|
||||
# return material with same name if it exists
|
||||
name = render_material['name'] || render_material['id']
|
||||
material = sketchup_model.materials[name]
|
||||
return material if material
|
||||
|
||||
# create a new sketchup material
|
||||
material = sketchup_model.materials.add(name)
|
||||
material.alpha = render_material['opacity']
|
||||
argb = render_material['diffuse']
|
||||
material.color = Sketchup::Color.new((argb >> 16) & 255, (argb >> 8) & 255, argb & 255, (argb >> 24) & 255)
|
||||
material
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def attribute_types
|
||||
|
||||
@@ -28,6 +28,20 @@ module SpeckleConnector
|
||||
)
|
||||
end
|
||||
|
||||
def self.to_native(t_arr, units)
|
||||
Geom::Transformation.new(
|
||||
[
|
||||
t_arr[0], t_arr[4], t_arr[8], t_arr[12],
|
||||
t_arr[1], t_arr[5], t_arr[9], t_arr[13],
|
||||
t_arr[2], t_arr[6], t_arr[10], t_arr[14],
|
||||
Geometry.length_to_native(t_arr[3], units),
|
||||
Geometry.length_to_native(t_arr[7], units),
|
||||
Geometry.length_to_native(t_arr[11], units),
|
||||
t_arr[15]
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def attribute_types
|
||||
|
||||
@@ -17,7 +17,7 @@ module SpeckleConnector
|
||||
end
|
||||
|
||||
def test_point_to_json
|
||||
point = Point.new(1.0, 1.0, 1.0, 'm')
|
||||
point = Point.from_coordinates(1.0, 1.0, 1.0, 'm')
|
||||
serialized_point = {
|
||||
speckle_type: 'Objects.Geometry.Point',
|
||||
units: 'm',
|
||||
|
||||
@@ -17,7 +17,7 @@ module SpeckleConnector
|
||||
end
|
||||
|
||||
def test_vector_to_json
|
||||
point = Vector.new(1.0, 1.0, 1.0, 'm')
|
||||
point = Vector.from_coordinates(1.0, 1.0, 1.0, 'm')
|
||||
serialized_point = {
|
||||
speckle_type: 'Objects.Geometry.Vector',
|
||||
units: 'm',
|
||||
|
||||
Reference in New Issue
Block a user