sqlitedb.tcl package
Purpose:
Database connectivity package to establish a connection to a remote sqlite database file via http protocol and with the aid of the SqliteDB PHP interface.
Be aware that this package is a proove of concept and far from complete.
Scenario:
The sqlite databasefile is stored on a remote drive shared by your internet provider.
With the aid of sqlitedb.php and sqlitedb.tcl it is possible to get access to the sqlite database in a similar manner as when working within a local environment.
database connectivity is managed over the http protocol and with apache + mod_php plus PHP sqlite extension (which nowadays is the default for a modern apache/PHP stack).
A client program can be developed which stores it’s database on the www (or on the intranet)!
Advantages:
ability to create web based client software, without the need of a browser, but with modern internet connectivity, whereas the client software interacts with a database (represented as an sqlite file) stored on a remote server,
rapid software development due to less dependencies,
let the administrator manage the database (with auto-backup, protection against deletion, etc…)
multi user friendly
Installation:
The sqlitedb.php file needs to be installed and configured correctly on the server (apache with mod_php and sqlite extension).
In other words, the php file needs to be copied on a valid directory onto the web-space of the provider and confugured as usual via .htaccess, etc.
And yes, as we use sqlite, there is no explicit database server required.
Code:
- pkgIndex.tcl
package ifneeded sqlitedb 0.1 \
"[list source [file join $dir sqlitedb.tcl]];"
- sqlitedb.tcl
# -----------------------------------------------------------------------------
# sqlitedb.tcl ---
# -----------------------------------------------------------------------------
# (c) 2018, Johann Oberdorfer - Engineering Support | CAD | Software
# johann.oberdorfer [at] gmail.com
# www.johann-oberdorfer.eu
# -----------------------------------------------------------------------------
# This source file is distributed under the BSD license.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the BSD License for more details.
# -----------------------------------------------------------------------------
#
# Purpose:
# Database connectivity package to establish a connection to a remote sqlite
# database file via http protocol and with the aid of the SqliteDB PHP
# interface.
# The SqliteDB PHP code needs to be installed and configures correctly on
# the server (apache with mod_php and sqlite extension).
#
# This package is a proove of concept.
# -----------------------------------------------------------------------------
#
# Revision history:
# 18-12-06: Hans, V0.1 - Initial release
# -----------------------------------------------------------------------------
# set dir [file dirname [file normalize [info script]]]
# lappend auto_path [file join $dir "lib"]
package require http
package require json
package provide sqlitedb 0.1
namespace eval ::sqlitedb:: {
# purpose of this function mainly is to stay compatible with the
# rest of our existing code
# note:
# json2dict returns a list of dicts
# this function converts to a "standard" data-list where the
# list only consinst of data items !
#
proc convert_dict2list {data} {
set new_datalist [list]
foreach item $data {
set item_list [list]
dict for {descr value} $item {
lappend item_list $value
}
lappend new_datalist $item_list
}
return $new_datalist
}
# hand over the sql string to PHP interface code via "sql" argument
#
# this way it makes it very easy to move away complex logic from the
# client application to the database server :=) ...
#
proc sqlEval {url sql} {
::http::config -urlencoding "utf-8"
puts \
"
>> $url
>> $sql
"
# ...instead of putting the data on the URL
# it's saver to put it in the body of a POST request:
set body $sql
# dict set hdr "Content-Type" "text/xml"
# ...once we've got the headers and the data, we can send the POST like this:
if { [catch {set handle [http::geturl $url -query $body]} errmsg] != 0 } {
append errmsg "\n\nPlease make shure your computer has a"
append errmsg "\nvalid internet connection and try again!"
tk_messageBox \
-title "Error with http connection:" \
-message $errmsg
-type ok -icon "error"
return {}
}
set data [::http::data $handle]
if { [string first "INSERT" [string toupper $sql]] != -1 ||
[string first "UPDATE" [string toupper $sql]] != -1 ||
[string first "DELETE" [string toupper $sql]] != -1 } {
::http::cleanup $handle
return [list $data]
}
# 1.) is there an error raised by the http layer?
# {
# "error": {
# "code": 204,
# "status": "No Content"
# }
# }
# dump the error message to stdout and we are done!
if { [::http::ncode $handle] != 200 } {
puts ">> Error occured: [::http::code $handle] - no data available."
::http::cleanup $handle
return {}
}
::http::cleanup $handle
# json2dict returns a list of dicts:
set jsondata [encoding convertfrom "utf-8" $data]
set ddata [::json::json2dict $jsondata]
# is there something going wrong with the returned data
# from the sqlitedb index.php interface ?
if { [lindex [lindex $ddata 0] 0] eq "error" } {
puts ">> SQL statement does not return any data."
return {}
}
# convert_dict2list might also raise an error,
# going to test 1st, if it's required to use catch here...
set ldata [convert_dict2list $ddata]
puts ">> Data list contains: [llength $ldata] item(s)."
return $ldata
}
proc is_valid_connection {url} {
::http::config -urlencoding "utf-8"
set query [::http::formatQuery "sql" "SELECT COUNT(*) FROM sqlite_master;"]
append url "?${query}"
if { [catch {set handle [::http::geturl $url]} errmsg] != 0 } {
return 0
}
# error handling: maybe this is not working as expected,
# missing database is created by the interface and ncode is still 200 !!!
# so better check against data lenght > 0 ?
# or something different (maybe improve error handling in foreign PHP code) ?...
if { [::http::ncode $handle] != 200 } {
return 0
}
set data [::http::data $handle]
if { [string first "error" $data] != -1 } {
return 0
}
# json2dict returns a list of dicts:
set ddict [lindex [::json::json2dict $data] 0]
if { [dict get $ddict "COUNT(*)"] == 0 } {
return 0
}
::http::cleanup $handle
return 1
}
# get the names of all the tables /views
# obj. could either be "table" or "view"
proc get_AllObjectNames {url {type "table"}} {
set sql \
"SELECT name FROM sqlite_master
WHERE type='$type'
AND name NOT LIKE 'sqlite_sequence'
AND name NOT LIKE '%_images'
UNION ALL SELECT name FROM sqlite_temp_master
WHERE type='$type'
ORDER BY name;"
set object_names [sqlEval $url $sql]
return $object_names
}
proc get_ColumnNames {url table_name} {
set column_names [list]
set data [sqlEval $url "PRAGMA table_info('$table_name');"]
# json:
# {
# {cid 0 name ID type INTEGER notnull 1 dflt_value null pk 1}
# {cid 1 name Photography type BOOLEAN notnull 0 dflt_value '' pk 0}
# ...
# }
# foreach item $data {
# dict for {descr value} $item {
# if {$descr eq "name"} { lappend column_names $value }
# }}
#
# list:
# {
# {0 ID INTEGER 1 null 1}
# {1 Photography BOOLEAN 0 '' 0}
# ...
# }
foreach item $data {
lappend column_names [lindex $item 1]
}
return $column_names
}
proc get_ColumnTypes {url table_name} {
set column_types [list]
set data [sqlEval $url "PRAGMA table_info('$table_name');"]
foreach item $data {
lappend column_types [lindex $item 2]
}
return $column_types
}
proc get_primarykey_index {url table_name} {
set data [sqlEval $url "PRAGMA table_info('$table_name');"]
set cnt -1
# cid name type null def pkey
foreach item $data {
set name [lindex $item 1]
set pkey [lindex $item end]
if {[string length $name] == 0} {
continue
}
incr cnt
if {$pkey == 1 } {
return $cnt
}
}
return -1
}
proc get_CreateStatement {url table_name} {
set sql \
"SELECT sql FROM
(SELECT * FROM sqlite_master UNION ALL
SELECT * FROM sqlite_temp_master)
WHERE tbl_name LIKE '$table_name'
AND type != 'meta'
ORDER BY type DESC, name;"
set data [lindex [lindex [sqlEval $url $sql] 0] 0]
return $data
}
}
- sqlitedb.php
<?php
/**
* The MIT License
* http://creativecommons.org/licenses/MIT/
*
* SqliteDB 0.1 ---
* -------------------------------------------------------------------------
* (c) 2018, Johann Oberdorfer - Engineering Support | CAD | Software
* johann.oberdorfer [at] gmail.com
* www.johann-oberdorfer.eu
* -------------------------------------------------------------------------
* This source file is distributed under the MIT license.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the BSD License for more details.
* -------------------------------------------------------------------------
* Credits:
* Great thanks to Alix Axel for his ArrestDB PHP interface.
* I used this code and basicly removed all code not related to sqlite,
* keeping only the interface for interaction with the sqlite database (file).
*
* The MIT License
* http://creativecommons.org/licenses/MIT/
*
* ArrestDB 1.9.0 (github.com/alixaxel/ArrestDB/)
* Copyright (c) 2014 Alix Axel <alix.axel@gmail.com>
**/
// these are the only settings required to configure the interface:
//
// $dsn = 'sqlite://./db/my_database.db';
// $clients = [];
$sql = file_get_contents('php://input');
// debugging purpose only,
// if enabled, be aware that "echo" would confuse
// the json return data and therfore would cause tcl's json converterto fail
//
// echo "<p>Sql: " . $sql . "</p>";
if ((empty($clients) !== true) && (in_array($_SERVER['REMOTE_ADDR'], (array) $clients) !== true))
{
exit(SqliteDB::Reply(SqliteDB::$HTTP[403]));
}
else if (SqliteDB::Query($dsn) === false)
{
exit(SqliteDB::Reply(SqliteDB::$HTTP[503]));
}
class SqliteDB
{
public static $HTTP = [
200 => ['success' => ['code' => 200, 'status' => 'OK', ],],
201 => ['success' => ['code' => 201, 'status' => 'Created', ],],
204 => ['error' => ['code' => 204, 'status' => 'No Content', ],],
400 => ['error' => ['code' => 400, 'status' => 'Bad Request',],],
403 => ['error' => ['code' => 403, 'status' => 'Forbidden', ],],
404 => ['error' => ['code' => 404, 'status' => 'Not Found', ],],
409 => ['error' => ['code' => 409, 'status' => 'Conflict', ],],
503 => ['error' => ['code' => 503, 'status' => 'Service Unavailable',],],
];
public static function Query($query = null)
{
static $db = null;
static $result = [];
try
{
if (isset($db, $query) === true)
{
if (empty($result[$hash = crc32($query)]) === true)
{
$result[$hash] = $db->prepare($query);
}
$data = array_slice(func_get_args(), 1);
if (count($data, COUNT_RECURSIVE) > count($data))
{
$data = iterator_to_array(new \RecursiveIteratorIterator(new \RecursiveArrayIterator($data)), false);
}
if ($result[$hash]->execute($data) === true)
{
$sequence = null;
switch (strstr($query, ' ', true))
{
case 'INSERT':
case 'REPLACE':
return $db->lastInsertId($sequence);
case 'UPDATE':
case 'DELETE':
return $result[$hash]->rowCount();
case 'SELECT':
case 'EXPLAIN':
case 'PRAGMA':
case 'SHOW':
return $result[$hash]->fetchAll();
}
return true;
}
return false;
}
else if (isset($query) === true)
{
$options = array
(
\PDO::ATTR_CASE => \PDO::CASE_NATURAL,
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
\PDO::ATTR_EMULATE_PREPARES => false,
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::ATTR_ORACLE_NULLS => \PDO::NULL_NATURAL,
\PDO::ATTR_STRINGIFY_FETCHES => false,
);
if (preg_match('~^sqlite://([[:print:]]++)$~i', $query, $dsn) > 0)
{
$options += array
(
\PDO::ATTR_TIMEOUT => 3,
);
$db = new \PDO(sprintf('sqlite:%s', $dsn[1]), null, null, $options);
$pragmas = array
(
'automatic_index' => 'ON',
'cache_size' => '8192',
'foreign_keys' => 'ON',
'journal_size_limit' => '67110000',
'locking_mode' => 'NORMAL',
'page_size' => '4096',
'recursive_triggers' => 'ON',
'secure_delete' => 'ON',
'synchronous' => 'NORMAL',
'temp_store' => 'MEMORY',
'journal_mode' => 'WAL',
'wal_autocheckpoint' => '4096',
);
if (strncasecmp(PHP_OS, 'WIN', 3) !== 0)
{
$memory = 131072;
if (($page = intval(shell_exec('getconf PAGESIZE'))) > 0)
{
$pragmas['page_size'] = $page;
}
if (is_readable('/proc/meminfo') === true)
{
if (is_resource($handle = fopen('/proc/meminfo', 'rb')) === true)
{
while (($line = fgets($handle, 1024)) !== false)
{
if (sscanf($line, 'MemTotal: %d kB', $memory) == 1)
{
$memory = round($memory / 131072) * 131072; break;
}
}
fclose($handle);
}
}
$pragmas['cache_size'] = intval($memory * 0.25 / ($pragmas['page_size'] / 1024));
$pragmas['wal_autocheckpoint'] = $pragmas['cache_size'] / 2;
}
foreach ($pragmas as $key => $value)
{
$db->exec(sprintf('PRAGMA %s=%s;', $key, $value));
}
}
}
}
catch (\Exception $exception)
{
return false;
}
return (isset($db) === true) ? $db : false;
}
public static function Reply($data)
{
$bitmask = 0;
$options = ['UNESCAPED_SLASHES', 'UNESCAPED_UNICODE'];
if (empty($_SERVER['HTTP_X_REQUESTED_WITH']) === true)
{
$options[] = 'PRETTY_PRINT';
}
foreach ($options as $option)
{
$bitmask |= (defined('JSON_' . $option) === true) ? constant('JSON_' . $option) : 0;
}
if (($result = json_encode($data, $bitmask)) !== false)
{
$callback = null;
if (array_key_exists('callback', $_GET) === true)
{
$callback = trim(preg_replace('~[^[:alnum:]\[\]_.]~', '', $_GET['callback']));
if (empty($callback) !== true)
{
$result = sprintf('%s(%s);', $callback, $result);
}
}
if (headers_sent() !== true)
{
header(sprintf('Content-Type: application/%s; charset=utf-8', (empty($callback) === true) ? 'json' : 'javascript'));
}
}
return $result;
}
}
$result = SqliteDB::Query($sql);
if ($result === false)
{
$result = SqliteDB::$HTTP[404];
}
else if (empty($result) === true)
{
$result = SqliteDB::$HTTP[204];
}
exit(SqliteDB::Reply($result));
?>
- sqlitedb_test.tcl
set dir [file dirname [file normalize [info script]]]
# specify, where to find support packages:
lappend auto_path [file join $dir "../../lib"]
lappend auto_path [file join $dir "."]
package require http
package require json
package require sqlitedb
catch {
console show
console eval {wm protocol . WM_DELETE_WINDOW {exit 0}}
}
# PHP tutorial:
# http://computer-programming-forum.com/57-tcl/f2b80bf59ac4ac6c.htm
if {0} {
# some arbitrary tests...
# open the URL to get the results:
::http::config -urlencoding "utf-8"
set handle [::http::geturl "http://php.johann-oberdorfer.eu/index.php/IdeasAndNotes"]
set body [::http::data $handle]
puts \
"
------------------------------------------------
return content:
$body
------------------------------------------------
"
set stat [::http::code $handle]
puts \
"
------------------------------------------------
return code:
$stat
------------------------------------------------
"
set headers [::http::meta $handle]
puts \
"
------------------------------------------------
return headers:
"
foreach {name value} $headers {
puts "$name: $value"
}
set data [encoding convertfrom "utf-8" [::http::data $handle]]
set data [::json::json2dict $data]
# set data [::json::many-json2dict $data]
foreach {name value} $data {
puts "$name: $value"
}
::http::cleanup $handle
}
# -INTERFACE TEST-
# note:
# the sqlite database name is already configured in the index.php file
# whereas index.php **is** the sqlitedb PHP interface for sqlite
set url "http://php.johann-oberdorfer.eu/index.php/"
if {0} {
puts [::sqlitedb::is_valid_connection $url]
set sql "SELECT \"ID\", \"Photography\", \"Programming\", \"Misc\", \"Tag\", \"DESCRIPTION\", \"NOTES\", \"% DONE\"
FROM \"IdeasAndNotes\";"
puts [::sqlitedb::sqlEval $url $sql]
puts [::sqlitedb::sqlEval $url "SELECT COUNT(*) FROM sqlite_master;"]
puts [::sqlitedb::get_AllObjectNames $url]
set table_names [::sqlitedb::get_AllObjectNames $url]
set table_name [lindex $table_names 0]
puts [::sqlitedb::get_ColumnNames $url $table_name]
puts [::sqlitedb::get_ColumnTypes $url $table_name]
puts [::sqlitedb::get_primarykey_index $url $table_name]
puts [::sqlitedb::get_CreateStatement $url $table_name]
}
set table_names [::sqlitedb::get_AllObjectNames $url]
foreach name $table_names {
puts "Table name: $name"
}
set table_name [lindex $table_names 0]
puts [::sqlitedb::get_ColumnNames $url $table_name]
puts [::sqlitedb::get_ColumnTypes $url $table_name]
Credits:
The sqlitedb.php package is derived from ArrestDB, which basicly interacts with the sqlite database on the remote server:
- ArrestDB 1.9.0 (github.com/alixaxel/ArrestDB/) Copyright (c) 2014 Alix Axel
Project status:
This package for the moment is a proove of concept.
Based on this software, I developed some kind of a to-do or task-list manager which is capable to share information amongst a team of developers (multi-user application).