Skip to content

Commit

Permalink
[1.9.11 beta] - 2024-03-05
Browse files Browse the repository at this point in the history
Fixed:
- A critical bug in duplicated panels and panels restored from a saved session - they sometimes had a different query applied when using automatic pagination than the query in the Query box.

Added:
- Foreign keys are now listed (in "Indexes", in both MariaDB/MySQL and PostgreSQL).
  • Loading branch information
Charlie Root committed Mar 5, 2024
1 parent f06bcf3 commit 4846e1d
Show file tree
Hide file tree
Showing 8 changed files with 339 additions and 29 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased]

## [1.9.11 beta] - 2024-03-05

### Fixed
- A critical bug in duplicated panels and panels restored from a saved session - they sometimes had a different query applied when using automatic pagination than the query in the Query box.

### Added
- Foreign keys are now listed (in "Indexes", in both MariaDB/MySQL and PostgreSQL).

## [1.9.10 beta] - 2024-02-19

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# SQLantern - The Multi-Panel Database Manager
Current version: v1.9.10β (public beta) | [Changelog](CHANGELOG.md)\
Current version: v1.9.11β (public beta) | [Changelog](CHANGELOG.md)\
License: [GNU General Public License v3.0](LICENSE)\
[Українською](README_uk.md)

Expand Down
2 changes: 1 addition & 1 deletion README_uk.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# SQLantern - Багатопанельний менеджер баз даних
Поточна версія: v1.9.10β (публічна бета) | [Changelog](CHANGELOG.md)\
Поточна версія: v1.9.11β (публічна бета) | [Changelog](CHANGELOG.md)\
Ліцензія: [GNU General Public License v3.0](LICENSE)

SQLantern - це менеджер баз даних (скоріше переглядач даних) в веббраузері, розроблений для високої продуктивності, із новою концепцією: відображення багатьох таблиць бази даних бік-о-бік в окремих панелях.\
Expand Down
3 changes: 3 additions & 0 deletions css/sqlantern.css
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,9 @@ body.no-hints [data-info] {
min-height: 138px; }
.one-tab .query-block .blocks-list > div.active {
display: block; }
.one-tab .query-block .history,
.one-tab .query-block .saved-queries {
word-break: break-word; }
.one-tab .icons-group label {
margin-right: 10px; }
.one-tab .one-line,
Expand Down
8 changes: 8 additions & 0 deletions js/sqlantern.js
Original file line number Diff line number Diff line change
Expand Up @@ -2532,6 +2532,10 @@ const state = {
tab.querySelector('.custom-name.active') ? obj.custom_name_class = true : '';
tab.querySelector('.table.rows .block-name.close') ? obj.block_rows_class = true : '';

if (tab.querySelector('.page-block')) {
obj.cur_query = tab.querySelector('.cur-query').value;
}

if (tab.querySelector('.custom-name')) {
const customName = tab.querySelector('.custom-name span').textContent;
const customInput = tab.querySelector('.custom-name input').value;
Expand Down Expand Up @@ -2825,6 +2829,10 @@ const restore = {
newTab.tab.querySelector('.query-block textarea').scrollTop = obj.textarea_scroll_top;
newTab.tab.querySelectorAll('.query-block .active').forEach(el => el.classList.remove('active'));

if (obj.cur_query) {
newTab.tab.querySelector('.cur-query').value = obj.cur_query;
}

if (obj.custom_name || obj.custom_name_input) {
newTab.tab.querySelector('.custom-name span').textContent = obj.custom_name;
newTab.tab.querySelector('.custom-name input').value = obj.custom_name_input;
Expand Down
57 changes: 33 additions & 24 deletions php/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,23 @@
*/

"SQL_DISPLAY_DATABASE_SIZES" => false,
// seeing the whole database sizes in the database list is very useful, but very often it's unbearably slow, so here's the option to enable it, but it's disabled by default
/*
Seeing the whole database sizes in the database list is very useful, but very often it's unbearably slow, so here's the option to enable it, but it's disabled by default.
*/

"SQL_NUMBER_FORMAT" => "builtInNumberFormat",
// a function name, which you can redefine to use your own
// used only for number of rows, number of pages, and number of unique values (in MariaDB/MySQL indexes list)!
// the function itself cannot be written right here as an anonymous function, only a function name, because constants can only store "simple values", unfortunately...
// NOTE . . . `SQL_NUMBER_FORMAT` will be removed in Version 2, because the number format will be customizable in config and in visual front side settings !!!
/*
The name of the function, which you can redefine to use your own.
Used only for number of rows, number of pages, and number of unique values (in MariaDB/MySQL indexes list)!
The function itself cannot be written right here as an anonymous function, only a function name, because constants can only store "simple values", unfortunately...
NOTE . . . `SQL_NUMBER_FORMAT` will be removed in Version 2, because the number format will be customizable in config and in visual front side settings !!!
*/

"SQL_BYTES_FORMAT" => "builtInBytesFormat",
// the same as above, but for bytes
// only used for databases' sizes and tables' sizes!
/*
The same as above, but for bytes.
Only used for databases' sizes and tables' sizes!
*/

"SQL_FAST_TABLE_ROWS" => true,
/*
Expand Down Expand Up @@ -131,15 +137,15 @@
" + " is SQLantern style.
", " is psql and Adminer style.
"," is pgAdmin style.
"," (no space) is pgAdmin style.
Using "\n" is possible (phpMyAdmin style), but requires additional CSS tuning to look even remotely acceptable.
*/

"SQL_MULTIHOST" => false,
/*
If `false`: will only connect to one (default) host (as set by `SQL_DEFAULT_HOST`; beware that it can be _any_ host, including remote).
If `true`: will try to connect to any host.
Default is `false`, because otherwise a copy would be easily used as a proxy for DDOS attacks on other servers out of the box, which is undesired.
Default is `false`, because otherwise a copy would be easily used as a proxy for brute force and/or DDOS attacks on other servers out of the box, which is undesired.
Note that is has nothing to do with default host being local or remote, the default host can be remote well and fine.
It only limits connections to one default host, or allows it to any host (local or remote).
*/
Expand All @@ -155,13 +161,13 @@

"SQL_SHORTENED_LENGTH" => 200,
/*
The length which long values are shortened to (amount of characters).
The length which long values are shortened to (number of characters).
*/

"SQL_SESSION_NAME" => "SQLANTERN_SESS_ID",
/*
It may sound far stretched, but configuring different `SQL_SESSION_NAME` allows using multiple instances of SQLantern in subdirectories on the same domain (with e.g. different default drivers, host limitations, etc), with possibility to separate access to them by IP, for example (on the web server level).
Official README contains some examples.
<del>Official README contains some examples.</del> (no, it doesn't; maybe it will one day)
*/

"SQL_COOKIE_NAME" => "sqlantern_client",
Expand All @@ -173,7 +179,7 @@

"SQL_DEDUPLICATE_COLUMNS" => true,
/*
Deduplicate columns with the same names, see function `deduplicateColumnNames` further below in this file for details.
Deduplicate columns which have the same name, see function `deduplicateColumnNames` further below in this file for details.
*/

"SQL_CIPHER_METHOD" => "aes-256-cbc", // encryption method to use for logins and passwords protection
Expand Down Expand Up @@ -220,7 +226,7 @@
Stage 2 will have a dialog to continue anyway if the user chooses to and possibly two internal memory options, but I don't really know yet.
*/

"SQL_VERSION" => "1.9.10 beta", // 24-02-19
"SQL_VERSION" => "1.9.11 beta", // 24-03-05
/*
Beware that DB modules have their own separate versions!
*/
Expand Down Expand Up @@ -271,6 +277,8 @@
And global-port will override global-driver.
That'll make possible e.g. one `SQL_RUN_AFTER_CONNECT` for `*:mysqli`, but another for an alternative port like `*:33306`, without specifying `33306` anywhere else. I hope I'll understand this thought later.
Hell knows how to make it without overcomplicating the configuraion :-(
function getSetting( $setting ) {
global $sys;
Expand All @@ -283,6 +291,9 @@ function queryMatches( $startOrEnd, $query, $compareTo ) {
The idea is to understand if the query ends with "LIMIT *", "LIMIT * OFFSET *", "LIMIT * *", etc
Is it really better than regex, though?
I'm just really afraid to screw up the regex big time...
// convert everything to lowercase, replace line breaks and commas with spaces, and break into words
//$words = ...
}
Expand Down Expand Up @@ -464,7 +475,7 @@ function respond() {
// XXX  

function fatalError( $msg, $pause = false ) {
if (function_exists("sqlDisconnect")) { // there are fatal errors without driver even loaded
if (function_exists("sqlDisconnect")) { // fatal errors sometimes happen without a driver even loaded
sqlDisconnect(); // would it be faster to not disconnect and let die by itself? does it really matter? :-D
}
if ($pause) {
Expand All @@ -485,7 +496,7 @@ function deduplicateColumnNames( $columns, $tables ) {
/*
It's typical to use associative arrays with SQL data in PHP, and lose some columns if multiple columns with the same name/alias are returned.
E.g. if a query `SELECT * FROM chats_chatters LEFT JOIN chats ON chats.id = chats_chatters.chat_id` returns multiple `id` columns, only the last `id` column is left, when using `mysqli_fetch_assoc`.
This is fine for pure PHP usage (you cannot have more than one array column or object property with the same name anyway), but it's different from the native SQL console results, it shows multiple columns with the same name all right.
This is fine for pure PHP usage (you cannot have more than one array column or object property with the same name anyway), but it's different from the native SQL console results, which shows multiple columns with the same name all right.
And it's sometimes confusing, becase I'm not always sure what is the source table of a column, and often not even aware that there are clashes (which I'd fix by selecting only what I need and maybe aliasing).
On the other hand, displaying multiple columns with the same name (like multiple `id`s) is also not clear enough, IMHO.
Expand Down Expand Up @@ -565,9 +576,9 @@ function builtInBytesFormat( $sizeBytes, $maxSize = 0 ) {
Also, "393.43MB" becomes "0.38GB", why not "0.39GB"?
"581.72MB" becomes "0.57GB", why not "0.58GB"?
"481.15MB" becomes "0.47GB", why not "0.48Gb"?
Ahhhh... it must be 1024, not 1000... all right, it makes sense.
Ahhhh... it must be because of 1024, not 1000... all right, it makes sense.
FIXME . . . Why does this function return "0.93Gb", but also "951.11MB" in the same list? :-D
FIXME . . . Why does this function return "0.93Gb", but also "951.11MB" _in the same list_? :-D
*/
// now, remove ONE trailing zero if any, to leave values like "176.0", but not "176.00"
$str = (substr($str, -1) == "0") ? substr($str, 0, -1) : $str;
Expand Down Expand Up @@ -648,7 +659,7 @@ function translation( $key = "???" ) {
$sys["translation"] = $translation["back-end"];
}

return isset($sys["translation"][$key]) ? $sys["translation"][$key] : "Translation not found: \"{$key}\""; // write a `key` of a missing translation, to find and fix it easily; there is NO adequate way to make THIS line multi-lingual... I mean, I could make a configurable constant for that, but seriously... it's only for developers/tranlators to signal a missing text...
return isset($sys["translation"][$key]) ? $sys["translation"][$key] : "Translation not found: \"{$key}\""; // write a `key` of a missing translation, to find and fix it easily; there is NO adequate way to make THIS line multi-lingual... I mean, I could make a configurable constant for that, but seriously... it's only for developers/tranlators to signal about a missing text...
}

// XXX  
Expand Down Expand Up @@ -697,7 +708,7 @@ function saveConnections() {

// JSON turned out to be text-only, completely unable to handle binary data, you live and learn...
// so, `serialize` it is...
// (I expected it to handle anything with some prefix.)
// (I expected it to handle anything with some prefix like it handles non-latin UTF symbols.)
setcookie(SQL_COOKIE_NAME, base64_encode(serialize($con)), 0, "/");
// ??? . . . do I care that it's a mix of `JSON`, `serialize` and `base64` formats?
}
Expand Down Expand Up @@ -726,8 +737,6 @@ function loadDriverByPort( $port ) {
/*
`port` is ALWAYS set internally, even if it is not used in the login string (the default port value is used in this case). It is never empty/unset.
This way `port` can ALWAYS be reliably used to select the database driver.
Fallback to the default driver if no definition for the port found.
*/
$drivers = json_decode(SQL_PORTS_TO_DRIVERS, true);

Expand Down Expand Up @@ -797,7 +806,7 @@ function loadDriverByPort( $port ) {
}


// have decrypted connections at hand in memory for multiple operations below (except for passwords, only one of which is decrypted at a time, for the chosen connection)
// keep decrypted connections at hand in memory for multiple operations below (except for passwords, only the one for the chosen connection is decrypted at a time)
$connections = [];

if (isset($_COOKIE[SQL_COOKIE_NAME])) {
Expand Down Expand Up @@ -854,8 +863,8 @@ function loadDriverByPort( $port ) {
/*
>>> THINKING HAT ON
Now, about password protection.
If the connections are fully stored at client, and server session only has a key, you'll need both parts.
But having client's part will allow to try bruteforce, because you might guess/know the connection names.
If the connections are fully stored at client and server session only has a key, you'll need both parts.
But having client's part will allow to try bruteforce it, because you might guess/know the connection names.
So, storing only passwords at client is also an option.
But then the server session will have connection names, which have logins, which is bad, as well.
Might make cross-encryption: client has encrypted passwords, server has encrypted logins, and they store keys to each other.
Expand Down
85 changes: 84 additions & 1 deletion php/php-mysqli.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php
/*
The base PHP lib/mysqli implementation for SQLantern by nekto
v1.0.7 beta | 24-02-06
v1.0.8 beta | 24-03-05
This file is part of SQLantern Database Manager
Copyright (C) 2022, 2023, 2024 Misha Grafski AKA nekto
Expand Down Expand Up @@ -228,6 +228,10 @@ function sqlEscape( $str ) {
function sqlListDb() {
//return sqlArray("SHOW DATABASES");
$res = sqlArray("SHOW DATABASES");
/*
FIXME . . . MySQL 5.5 @ pi returns the non-alphabetically sorted list - it displays `information_schema` above all other databases.
So, an additional alphabetical sort must be applied.
*/

if (SQL_DISPLAY_DATABASE_SIZES) {
$bytesFormat = SQL_BYTES_FORMAT; // constants can't be used directly as functions
Expand Down Expand Up @@ -566,6 +570,85 @@ function ($v) use ($singleKey) {
}
unset($s);


/*
Add foreign keys to the end of the list.
An important note: "MySQL requires that foreign key columns be indexed; if you create a table with a foreign key constraint but no index on a given column, an index is created."
The consequence is that creating a foreign key on a non-indexed column creates a new local index as well, and you get both index in the table AND a foreign index, which confusingly have the same name (as far as I see).
The query I started with was:
SELECT
-- table_schema, table_name, column_name, constraint_name
-- *
table_schema, table_name, column_name, constraint_name,
referenced_table_schema, referenced_table_name, referenced_column_name
FROM information_schema.key_column_usage
WHERE table_schema = '{database}'
AND table_name = '{table}'
AND referenced_table_schema IS NOT NULL
And my final version (list all foreign keys from the whole database):
SELECT
table_name,
constraint_name,
GROUP_CONCAT(column_name SEPARATOR ' [x] ') AS columns,
CONCAT(
IF(referenced_table_schema != table_schema, CONCAT(referenced_table_schema, '.'), ''),
referenced_table_name, '.',
GROUP_CONCAT(referenced_column_name SEPARATOR ' [x] ')
) AS ref
FROM information_schema.key_column_usage
WHERE table_schema = '{database}'
AND referenced_table_schema IS NOT NULL
GROUP BY table_name, constraint_name
ORDER BY table_name ASC, constraint_name ASC
NOTE . . . Adminer is the only one I know which lists foreign keys in a table like I do, and it's syntax is:
Source Target
ndb_no, nutr_no nut_data(ndb_no, nutr_no)
Which is not bad at all - similar to the creating foreign key syntax.
I like mine better for now, but I should keep in mind that way of displaying it, too. Maybe I'll like it better one day (or the users).
*/

$indexConcatenator = sqlEscape(SQL_INDEX_COLUMNS_CONCATENATOR);
$foreign = sqlArray("
SELECT
constraint_name,
GROUP_CONCAT(column_name SEPARATOR '{$indexConcatenator}') AS columns,
CONCAT(
IF(referenced_table_schema != table_schema, CONCAT(referenced_table_schema, '.'), ''),
referenced_table_name, '.',
GROUP_CONCAT(referenced_column_name SEPARATOR '{$indexConcatenator}')
) AS ref
FROM information_schema.key_column_usage
WHERE table_schema = '{$databaseName}'
AND table_name = '{$tableName}'
AND referenced_table_schema IS NOT NULL
GROUP BY constraint_name
ORDER BY constraint_name ASC
");

if ($foreign) {
// indexes get an additional column if the table has foreign keys
foreach ($indexes as &$i) {
$i["Foreign reference"] = "";
}
unset($i);

foreach ($foreign as $f) {
$indexes[] = [
"Index" => $f["constraint_name"],
"Columns" => $f["columns"],
"Primary" => "", // "n/a" looks bad, IMHO
"Unique" => "", // same thing
"Cardinality" => "",
"Foreign reference" => $f["ref"],
];
}
}


return [
"structure" => $structure,

Expand Down
Loading

0 comments on commit 4846e1d

Please sign in to comment.