Add the ".lint fkey-indexes" command to the command-line shell.
FossilOrigin-Name: 94689e3bdac2eabbcf1a51d741c2604ed4bd8a40
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
C Built-in\sPRAGMA\sstatements\swithout\sside-effects\scan\sbe\sinvoked\sas\ntable-valued\sfunctions\sby\sprefixing\stheir\sname\swith\s"pragma_".
|
||||
D 2016-12-16T18:14:19.741
|
||||
C Add\sthe\s".lint\sfkey-indexes"\scommand\sto\sthe\scommand-line\sshell.
|
||||
D 2016-12-16T18:43:49.329
|
||||
F Makefile.in c194b58fe00c370a48ac6ae6945e92a7781db1c8
|
||||
F Makefile.linux-gcc 7bc79876b875010e8c8f9502eb935ca92aa3c434
|
||||
F Makefile.msc b8ca53350ae545e3562403d5da2a69cec79308da
|
||||
@@ -389,7 +389,7 @@ F src/random.c 80f5d666f23feb3e6665a6ce04c7197212a88384
|
||||
F src/resolve.c bb070cf5f23611c44ab7e4788803684e385fc3fb
|
||||
F src/rowset.c 7b7e7e479212e65b723bf40128c7b36dc5afdfac
|
||||
F src/select.c e4c687333c5fa33b1ce05c37bfbea6e09d3dbee9
|
||||
F src/shell.c f1c7fde7a83421e6ed620df41d200674a2327e65
|
||||
F src/shell.c 9be556ad9c7cf429d0acc48e386960d290c4df75
|
||||
F src/sqlite.h.in e8e2d108d82647f0a812fdb74accf91c1ec08ddc
|
||||
F src/sqlite3.rc 5121c9e10c3964d5755191c80dd1180c122fc3a8
|
||||
F src/sqlite3ext.h 8648034aa702469afb553231677306cc6492a1ae
|
||||
@@ -1094,6 +1094,7 @@ F test/shell2.test e242a9912f44f4c23c3d1d802a83e934e84c853b
|
||||
F test/shell3.test 9b95ba643eaa228376f06a898fb410ee9b6e57c1
|
||||
F test/shell4.test 89ad573879a745974ff2df20ff97c5d6ffffbd5d
|
||||
F test/shell5.test 50a732c1c2158b1cd62cf53975ce1ea7ce6b9dc9
|
||||
F test/shell6.test 806472f6ffdd31d632e27dbe8515c0c45f68f3ec
|
||||
F test/shortread1.test bb591ef20f0fd9ed26d0d12e80eee6d7ac8897a3
|
||||
F test/show_speedtest1_rtree.tcl 32e6c5f073d7426148a6936a0408f4b5b169aba5
|
||||
F test/shrink.test 1b4330b1fd9e818c04726d45cb28db73087535ce
|
||||
@@ -1536,8 +1537,8 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93
|
||||
F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc
|
||||
F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e
|
||||
F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0
|
||||
P 053a149cc8244a7f85137129cfcb8622efe90306 546821e29edb0282a4b1d8f49512e36027a6bf6d
|
||||
R 902244b69c72f2e09bae5fc96acaaf29
|
||||
T +closed 546821e29edb0282a4b1d8f49512e36027a6bf6d
|
||||
P d66ec5cfb67c6c8134fb642e0c2f72113fe1b7fd 1268dc77712d85de0b62332c88b22f7489f4e87f
|
||||
R 687c3d19b933c4e396bfa9c657e5c87e
|
||||
T +closed 1268dc77712d85de0b62332c88b22f7489f4e87f
|
||||
U drh
|
||||
Z fa65eb61f6dc861fd37d32c60a04f1db
|
||||
Z fa4bf48b88eaf725bf36396e1a6dc597
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
d66ec5cfb67c6c8134fb642e0c2f72113fe1b7fd
|
||||
94689e3bdac2eabbcf1a51d741c2604ed4bd8a40
|
||||
+254
@@ -2199,6 +2199,8 @@ static char zHelp[] =
|
||||
".iotrace FILE Enable I/O diagnostic logging to FILE\n"
|
||||
#endif
|
||||
".limit ?LIMIT? ?VAL? Display or change the value of an SQLITE_LIMIT\n"
|
||||
".lint OPTIONS Report potential schema issues. Options:\n"
|
||||
" fkey-indexes Find missing foreign key indexes\n"
|
||||
#ifndef SQLITE_OMIT_LOAD_EXTENSION
|
||||
".load FILE ?ENTRY? Load an extension library\n"
|
||||
#endif
|
||||
@@ -3253,6 +3255,253 @@ int shellDeleteFile(const char *zFilename){
|
||||
return rc;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
** The implementation of SQL scalar function fkey_collate_clause(), used
|
||||
** by the ".lint fkey-indexes" command. This scalar function is always
|
||||
** called with four arguments - the parent table name, the parent column name,
|
||||
** the child table name and the child column name.
|
||||
**
|
||||
** fkey_collate_clause('parent-tab', 'parent-col', 'child-tab', 'child-col')
|
||||
**
|
||||
** If either of the named tables or columns do not exist, this function
|
||||
** returns an empty string. An empty string is also returned if both tables
|
||||
** and columns exist but have the same default collation sequence. Or,
|
||||
** if both exist but the default collation sequences are different, this
|
||||
** function returns the string " COLLATE <parent-collation>", where
|
||||
** <parent-collation> is the default collation sequence of the parent column.
|
||||
*/
|
||||
static void shellFkeyCollateClause(
|
||||
sqlite3_context *pCtx,
|
||||
int nVal,
|
||||
sqlite3_value **apVal
|
||||
){
|
||||
sqlite3 *db = sqlite3_context_db_handle(pCtx);
|
||||
const char *zParent;
|
||||
const char *zParentCol;
|
||||
const char *zParentSeq;
|
||||
const char *zChild;
|
||||
const char *zChildCol;
|
||||
const char *zChildSeq;
|
||||
int rc;
|
||||
|
||||
assert( nVal==4 );
|
||||
zParent = (const char*)sqlite3_value_text(apVal[0]);
|
||||
zParentCol = (const char*)sqlite3_value_text(apVal[1]);
|
||||
zChild = (const char*)sqlite3_value_text(apVal[2]);
|
||||
zChildCol = (const char*)sqlite3_value_text(apVal[3]);
|
||||
|
||||
sqlite3_result_text(pCtx, "", -1, SQLITE_STATIC);
|
||||
rc = sqlite3_table_column_metadata(
|
||||
db, "main", zParent, zParentCol, 0, &zParentSeq, 0, 0, 0
|
||||
);
|
||||
if( rc==SQLITE_OK ){
|
||||
rc = sqlite3_table_column_metadata(
|
||||
db, "main", zChild, zChildCol, 0, &zChildSeq, 0, 0, 0
|
||||
);
|
||||
}
|
||||
|
||||
if( rc==SQLITE_OK && sqlite3_stricmp(zParentSeq, zChildSeq) ){
|
||||
char *z = sqlite3_mprintf(" COLLATE %s", zParentSeq);
|
||||
sqlite3_result_text(pCtx, z, -1, SQLITE_TRANSIENT);
|
||||
sqlite3_free(z);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
** The implementation of dot-command ".lint fkey-indexes".
|
||||
*/
|
||||
static int lintFkeyIndexes(
|
||||
ShellState *pState, /* Current shell tool state */
|
||||
char **azArg, /* Array of arguments passed to dot command */
|
||||
int nArg /* Number of entries in azArg[] */
|
||||
){
|
||||
sqlite3 *db = pState->db; /* Database handle to query "main" db of */
|
||||
FILE *out = pState->out; /* Stream to write non-error output to */
|
||||
int bVerbose = 0; /* If -verbose is present */
|
||||
int bGroupByParent = 0; /* If -groupbyparent is present */
|
||||
int i; /* To iterate through azArg[] */
|
||||
const char *zIndent = ""; /* How much to indent CREATE INDEX by */
|
||||
int rc; /* Return code */
|
||||
sqlite3_stmt *pSql = 0; /* Compiled version of SQL statement below */
|
||||
|
||||
/*
|
||||
** This SELECT statement returns one row for each foreign key constraint
|
||||
** in the schema of the main database. The column values are:
|
||||
**
|
||||
** 0. The text of an SQL statement similar to:
|
||||
**
|
||||
** "EXPLAIN QUERY PLAN SELECT rowid FROM child_table WHERE child_key=?"
|
||||
**
|
||||
** This is the same SELECT that the foreign keys implementation needs
|
||||
** to run internally on child tables. If there is an index that can
|
||||
** be used to optimize this query, then it can also be used by the FK
|
||||
** implementation to optimize DELETE or UPDATE statements on the parent
|
||||
** table.
|
||||
**
|
||||
** 1. A GLOB pattern suitable for sqlite3_strglob(). If the plan output by
|
||||
** the EXPLAIN QUERY PLAN command matches this pattern, then the schema
|
||||
** contains an index that can be used to optimize the query.
|
||||
**
|
||||
** 2. Human readable text that describes the child table and columns. e.g.
|
||||
**
|
||||
** "child_table(child_key1, child_key2)"
|
||||
**
|
||||
** 3. Human readable text that describes the parent table and columns. e.g.
|
||||
**
|
||||
** "parent_table(parent_key1, parent_key2)"
|
||||
**
|
||||
** 4. A full CREATE INDEX statement for an index that could be used to
|
||||
** optimize DELETE or UPDATE statements on the parent table. e.g.
|
||||
**
|
||||
** "CREATE INDEX child_table_child_key ON child_table(child_key)"
|
||||
**
|
||||
** 5. The name of the parent table.
|
||||
**
|
||||
** These six values are used by the C logic below to generate the report.
|
||||
*/
|
||||
const char *zSql =
|
||||
"SELECT "
|
||||
" 'EXPLAIN QUERY PLAN SELECT rowid FROM ' || quote(s.name) || ' WHERE '"
|
||||
" || group_concat(quote(s.name) || '.' || quote(f.[from]) || '=?' "
|
||||
" || fkey_collate_clause(f.[table], f.[to], s.name, f.[from]),' AND ')"
|
||||
", "
|
||||
" 'SEARCH TABLE ' || s.name || ' USING COVERING INDEX*('"
|
||||
" || group_concat('*=?', ' AND ') || ')'"
|
||||
", "
|
||||
" s.name || '(' || group_concat(f.[from], ', ') || ')'"
|
||||
", "
|
||||
" f.[table] || '(' || group_concat(COALESCE(f.[to], "
|
||||
" (SELECT name FROM pragma_table_info(f.[table]) WHERE pk=seq+1)"
|
||||
" )) || ')'"
|
||||
", "
|
||||
" 'CREATE INDEX ' || quote(s.name ||'_'|| group_concat(f.[from], '_'))"
|
||||
" || ' ON ' || quote(s.name) || '('"
|
||||
" || group_concat(quote(f.[from]) ||"
|
||||
" fkey_collate_clause(f.[table], f.[to], s.name, f.[from]), ', ')"
|
||||
" || ');'"
|
||||
", "
|
||||
" f.[table] "
|
||||
|
||||
"FROM sqlite_master AS s, pragma_foreign_key_list(s.name) AS f "
|
||||
"GROUP BY s.name, f.id "
|
||||
"ORDER BY (CASE WHEN ? THEN f.[table] ELSE s.name END)"
|
||||
;
|
||||
|
||||
for(i=2; i<nArg; i++){
|
||||
int n = strlen(azArg[i]);
|
||||
if( n>1 && sqlite3_strnicmp("-verbose", azArg[i], n)==0 ){
|
||||
bVerbose = 1;
|
||||
}
|
||||
else if( n>1 && sqlite3_strnicmp("-groupbyparent", azArg[i], n)==0 ){
|
||||
bGroupByParent = 1;
|
||||
zIndent = " ";
|
||||
}
|
||||
else{
|
||||
raw_printf(stderr, "Usage: %s %s ?-verbose? ?-groupbyparent?\n",
|
||||
azArg[0], azArg[1]
|
||||
);
|
||||
return SQLITE_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
/* Register the fkey_collate_clause() SQL function */
|
||||
rc = sqlite3_create_function(db, "fkey_collate_clause", 4, SQLITE_UTF8,
|
||||
0, shellFkeyCollateClause, 0, 0
|
||||
);
|
||||
|
||||
|
||||
if( rc==SQLITE_OK ){
|
||||
rc = sqlite3_prepare_v2(db, zSql, -1, &pSql, 0);
|
||||
}
|
||||
if( rc==SQLITE_OK ){
|
||||
sqlite3_bind_int(pSql, 1, bGroupByParent);
|
||||
}
|
||||
|
||||
if( rc==SQLITE_OK ){
|
||||
int rc2;
|
||||
char *zPrev = 0;
|
||||
while( SQLITE_ROW==sqlite3_step(pSql) ){
|
||||
int res = -1;
|
||||
sqlite3_stmt *pExplain = 0;
|
||||
const char *zEQP = (const char*)sqlite3_column_text(pSql, 0);
|
||||
const char *zGlob = (const char*)sqlite3_column_text(pSql, 1);
|
||||
const char *zFrom = (const char*)sqlite3_column_text(pSql, 2);
|
||||
const char *zTarget = (const char*)sqlite3_column_text(pSql, 3);
|
||||
const char *zCI = (const char*)sqlite3_column_text(pSql, 4);
|
||||
const char *zParent = (const char*)sqlite3_column_text(pSql, 5);
|
||||
|
||||
rc = sqlite3_prepare_v2(db, zEQP, -1, &pExplain, 0);
|
||||
if( rc!=SQLITE_OK ) break;
|
||||
if( SQLITE_ROW==sqlite3_step(pExplain) ){
|
||||
const char *zPlan = (const char*)sqlite3_column_text(pExplain, 3);
|
||||
res = (0==sqlite3_strglob(zGlob, zPlan));
|
||||
}
|
||||
rc = sqlite3_finalize(pExplain);
|
||||
if( rc!=SQLITE_OK ) break;
|
||||
|
||||
if( res<0 ){
|
||||
raw_printf(stderr, "Error: internal error");
|
||||
break;
|
||||
}else{
|
||||
if( bGroupByParent
|
||||
&& (bVerbose || res==0)
|
||||
&& (zPrev==0 || sqlite3_stricmp(zParent, zPrev))
|
||||
){
|
||||
raw_printf(out, "-- Parent table %s\n", zParent);
|
||||
sqlite3_free(zPrev);
|
||||
zPrev = sqlite3_mprintf("%s", zParent);
|
||||
}
|
||||
|
||||
if( res==0 ){
|
||||
raw_printf(out, "%s%s --> %s\n", zIndent, zCI, zTarget);
|
||||
}else if( bVerbose ){
|
||||
raw_printf(out, "%s/* no extra indexes required for %s -> %s */\n",
|
||||
zIndent, zFrom, zTarget
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
sqlite3_free(zPrev);
|
||||
|
||||
if( rc!=SQLITE_OK ){
|
||||
raw_printf(stderr, "%s\n", sqlite3_errmsg(db));
|
||||
}
|
||||
|
||||
rc2 = sqlite3_finalize(pSql);
|
||||
if( rc==SQLITE_OK && rc2!=SQLITE_OK ){
|
||||
rc = rc2;
|
||||
raw_printf(stderr, "%s\n", sqlite3_errmsg(db));
|
||||
}
|
||||
}else{
|
||||
raw_printf(stderr, "%s\n", sqlite3_errmsg(db));
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
/*
|
||||
** Implementation of ".lint" dot command.
|
||||
*/
|
||||
static int lintDotCommand(
|
||||
ShellState *pState, /* Current shell tool state */
|
||||
char **azArg, /* Array of arguments passed to dot command */
|
||||
int nArg /* Number of entries in azArg[] */
|
||||
){
|
||||
int n;
|
||||
n = (nArg>=2 ? strlen(azArg[1]) : 0);
|
||||
if( n<1 || sqlite3_strnicmp(azArg[1], "fkey-indexes", n) ) goto usage;
|
||||
return lintFkeyIndexes(pState, azArg, nArg);
|
||||
|
||||
usage:
|
||||
raw_printf(stderr, "Usage %s sub-command ?switches...?\n", azArg[0]);
|
||||
raw_printf(stderr, "Where sub-commands are:\n");
|
||||
raw_printf(stderr, " fkey-indexes\n");
|
||||
return SQLITE_ERROR;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
** If an input line begins with "." then invoke this routine to
|
||||
** process that line.
|
||||
@@ -4033,6 +4282,11 @@ static int do_meta_command(char *zLine, ShellState *p){
|
||||
}
|
||||
}else
|
||||
|
||||
if( c=='l' && n>2 && strncmp(azArg[0], "lint", n)==0 ){
|
||||
open_db(p, 0);
|
||||
lintDotCommand(p, azArg, nArg);
|
||||
}else
|
||||
|
||||
#ifndef SQLITE_OMIT_LOAD_EXTENSION
|
||||
if( c=='l' && strncmp(azArg[0], "load", n)==0 ){
|
||||
const char *zFile, *zProc;
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# 2016 December 15
|
||||
#
|
||||
# The author disclaims copyright to this source code. In place of
|
||||
# a legal notice, here is a blessing:
|
||||
#
|
||||
# May you do good and not evil.
|
||||
# May you find forgiveness for yourself and forgive others.
|
||||
# May you share freely, never taking more than you give.
|
||||
#
|
||||
#***********************************************************************
|
||||
#
|
||||
|
||||
set testdir [file dirname $argv0]
|
||||
source $testdir/tester.tcl
|
||||
set testprefix shell6
|
||||
set CLI [test_find_cli]
|
||||
db close
|
||||
forcedelete test.db test.db-journal test.db-wal
|
||||
|
||||
foreach {tn schema output} {
|
||||
1 {
|
||||
CREATE TABLE p1(a PRIMARY KEY, b);
|
||||
CREATE TABLE c1(x, y REFERENCES p1);
|
||||
} {
|
||||
CREATE INDEX 'c1_y' ON 'c1'('y'); --> p1(a)
|
||||
}
|
||||
|
||||
2 {
|
||||
CREATE TABLE p1(a PRIMARY KEY, b);
|
||||
CREATE TABLE c2(x REFERENCES p1, y REFERENCES p1);
|
||||
} {
|
||||
CREATE INDEX 'c2_y' ON 'c2'('y'); --> p1(a)
|
||||
CREATE INDEX 'c2_x' ON 'c2'('x'); --> p1(a)
|
||||
}
|
||||
|
||||
3 {
|
||||
CREATE TABLE 'p 1'(a, b, c, PRIMARY KEY(c, b));
|
||||
CREATE TABLE 'c 1'(x, y, z, FOREIGN KEY (z, y) REFERENCES 'p 1');
|
||||
} {
|
||||
CREATE INDEX 'c 1_z_y' ON 'c 1'('z', 'y'); --> p 1(c,b)
|
||||
}
|
||||
|
||||
4 {
|
||||
CREATE TABLE p1(a, 'b b b' PRIMARY KEY);
|
||||
CREATE TABLE c1('x y z' REFERENCES p1);
|
||||
CREATE INDEX i1 ON c1('x y z') WHERE "x y z" IS NOT NULL;
|
||||
} {
|
||||
}
|
||||
|
||||
5 {
|
||||
CREATE TABLE p1(a, 'b b b' PRIMARY KEY);
|
||||
CREATE TABLE c1('x y z' REFERENCES p1);
|
||||
CREATE INDEX i1 ON c1('x y z') WHERE "x y z" IS NOT 12;
|
||||
} {
|
||||
CREATE INDEX 'c1_x y z' ON 'c1'('x y z'); --> p1(b b b)
|
||||
}
|
||||
|
||||
6 {
|
||||
CREATE TABLE x1(a, b, c, UNIQUE(a, b));
|
||||
CREATE TABLE y1(a, b, c, FOREIGN KEY(b, a) REFERENCES x1(a, b));
|
||||
CREATE INDEX y1i ON y1(a, c, b);
|
||||
} {
|
||||
CREATE INDEX 'y1_b_a' ON 'y1'('b', 'a'); --> x1(a,b)
|
||||
}
|
||||
|
||||
6 {
|
||||
CREATE TABLE x1(a COLLATE nocase, b, UNIQUE(a));
|
||||
CREATE TABLE y1(a COLLATE rtrim REFERENCES x1(a));
|
||||
} {
|
||||
CREATE INDEX 'y1_a' ON 'y1'('a' COLLATE nocase); --> x1(a)
|
||||
}
|
||||
|
||||
} {
|
||||
forcedelete test.db
|
||||
sqlite3 db test.db
|
||||
execsql $schema
|
||||
|
||||
set expected ""
|
||||
foreach line [split $output "\n"] {
|
||||
set line [string trim $line]
|
||||
if {$line!=""} {
|
||||
append expected "$line\n"
|
||||
}
|
||||
}
|
||||
|
||||
do_test 1.$tn.1 {
|
||||
set RES [catchcmd test.db [list .lint fkey-indexes]]
|
||||
} [list 0 [string trim $expected]]
|
||||
|
||||
do_test 1.$tn.2 {
|
||||
execsql [lindex $RES 1]
|
||||
catchcmd test.db [list .lint fkey-indexes]
|
||||
} {0 {}}
|
||||
|
||||
db close
|
||||
}
|
||||
|
||||
finish_test
|
||||
|
||||
|
||||
Reference in New Issue
Block a user