Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
Comment: | Store schema cookies on the TEMP database. Ticket #807. (CVS 1817) |
---|---|
Downloads: | Tarball | ZIP archive |
Timelines: | family | ancestors | descendants | both | trunk |
Files: | files | file ages | folders |
SHA1: |
c6c13dc460094e6adea2b14849edf9f4 |
User & Date: | drh 2004-07-19 17:25:25.000 |
Context
2004-07-19
| ||
19:14 | Fix for ticket #813. (CVS 1818) (check-in: 88e4bfa154 user: drh tags: trunk) | |
17:25 | Store schema cookies on the TEMP database. Ticket #807. (CVS 1817) (check-in: c6c13dc460 user: drh tags: trunk) | |
04:25 | use -lsqlite3 in .pc file (CVS 1816) (check-in: b36e6e4907 user: dougcurrie tags: trunk) | |
Changes
Changes to src/build.c.
︙ | ︙ | |||
19 20 21 22 23 24 25 | ** DROP INDEX ** creating ID lists ** BEGIN TRANSACTION ** COMMIT ** ROLLBACK ** PRAGMA ** | | | 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | ** DROP INDEX ** creating ID lists ** BEGIN TRANSACTION ** COMMIT ** ROLLBACK ** PRAGMA ** ** $Id: build.c,v 1.238 2004/07/19 17:25:25 drh Exp $ */ #include "sqliteInt.h" #include <ctype.h> /* ** This routine is called when a new SQL statement is beginning to ** be parsed. Check to see if the schema for the database needs |
︙ | ︙ | |||
65 66 67 68 69 70 71 | /* The cookie mask contains one bit for each database file open. ** (Bit 0 is for main, bit 1 is for temp, and so forth.) Bits are ** set for each database that is used. Generate code to start a ** transaction on each used database and to verify the schema cookie ** on each used database. */ | | | < | | < | > | 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | /* The cookie mask contains one bit for each database file open. ** (Bit 0 is for main, bit 1 is for temp, and so forth.) Bits are ** set for each database that is used. Generate code to start a ** transaction on each used database and to verify the schema cookie ** on each used database. */ if( pParse->cookieGoto>0 ){ u32 mask; int iDb; sqlite3VdbeChangeP2(v, pParse->cookieGoto-1, sqlite3VdbeCurrentAddr(v)); for(iDb=0, mask=1; iDb<db->nDb; mask<<=1, iDb++){ if( (mask & pParse->cookieMask)==0 ) continue; sqlite3VdbeAddOp(v, OP_Transaction, iDb, (mask & pParse->writeMask)!=0); sqlite3VdbeAddOp(v, OP_VerifyCookie, iDb, pParse->cookieValue[iDb]); } sqlite3VdbeAddOp(v, OP_Goto, 0, pParse->cookieGoto); } } /* Get the VDBE program ready for execution */ if( v && pParse->nErr==0 ){ FILE *trace = (db->flags & SQLITE_VdbeTrace)!=0 ? stdout : 0; sqlite3VdbeTrace(v, trace); sqlite3VdbeMakeReady(v, pParse->nVar, pParse->explain); pParse->rc = pParse->nErr ? SQLITE_ERROR : SQLITE_DONE; pParse->colNamesSet = 0; }else if( pParse->rc==SQLITE_OK ){ pParse->rc = SQLITE_ERROR; } pParse->nTab = 0; pParse->nMem = 0; pParse->nSet = 0; pParse->nAgg = 0; pParse->nVar = 0; pParse->cookieMask = 0; pParse->cookieGoto = 0; } /* ** Locate the in-memory structure that describes a particular database ** table given the name of that table and (optionally) the name of the ** database containing the table. Return NULL if not found. ** |
︙ | ︙ | |||
1374 1375 1376 1377 1378 1379 1380 | n = Addr(pEnd->z) - Addr(pParse->sNameToken.z) + 1; sqlite3VdbeAddOp(v, OP_String8, 0, 0); sqlite3VdbeChangeP3(v, -1, pParse->sNameToken.z, n); sqlite3VdbeAddOp(v, OP_Concat8, 2, 0); } sqlite3VdbeOp3(v, OP_MakeRecord, 5, 0, "tttit", P3_STATIC); sqlite3VdbeAddOp(v, OP_PutIntKey, 0, 0); | < | < | 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 | n = Addr(pEnd->z) - Addr(pParse->sNameToken.z) + 1; sqlite3VdbeAddOp(v, OP_String8, 0, 0); sqlite3VdbeChangeP3(v, -1, pParse->sNameToken.z, n); sqlite3VdbeAddOp(v, OP_Concat8, 2, 0); } sqlite3VdbeOp3(v, OP_MakeRecord, 5, 0, "tttit", P3_STATIC); sqlite3VdbeAddOp(v, OP_PutIntKey, 0, 0); sqlite3ChangeCookie(db, v, p->iDb); sqlite3VdbeAddOp(v, OP_Close, 0, 0); sqlite3EndWriteOperation(pParse); } /* Add the table to the in-memory representation of the database. */ |
︙ | ︙ | |||
2194 2195 2196 2197 2198 2199 2200 | "indexed columns are not unique", P3_STATIC); sqlite3VdbeAddOp(v, OP_Next, 2, lbl1); sqlite3VdbeResolveLabel(v, lbl2); sqlite3VdbeAddOp(v, OP_Close, 2, 0); sqlite3VdbeAddOp(v, OP_Close, 1, 0); } if( pTblName!=0 ){ | < | < | 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 | "indexed columns are not unique", P3_STATIC); sqlite3VdbeAddOp(v, OP_Next, 2, lbl1); sqlite3VdbeResolveLabel(v, lbl2); sqlite3VdbeAddOp(v, OP_Close, 2, 0); sqlite3VdbeAddOp(v, OP_Close, 1, 0); } if( pTblName!=0 ){ sqlite3ChangeCookie(db, v, iDb); sqlite3VdbeAddOp(v, OP_Close, 0, 0); sqlite3EndWriteOperation(pParse); } } /* When adding an index to the list of indices for a table, make ** sure all indices labeled OE_Replace come after all those labeled |
︙ | ︙ | |||
2296 2297 2298 2299 2300 2301 2302 | }; int base; sqlite3BeginWriteOperation(pParse, 0, pIndex->iDb); sqlite3OpenMasterTable(v, pIndex->iDb); base = sqlite3VdbeAddOpList(v, ArraySize(dropIndex), dropIndex); sqlite3VdbeChangeP3(v, base+1, pIndex->zName, 0); | < | < | 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 | }; int base; sqlite3BeginWriteOperation(pParse, 0, pIndex->iDb); sqlite3OpenMasterTable(v, pIndex->iDb); base = sqlite3VdbeAddOpList(v, ArraySize(dropIndex), dropIndex); sqlite3VdbeChangeP3(v, base+1, pIndex->zName, 0); sqlite3ChangeCookie(db, v, pIndex->iDb); sqlite3VdbeAddOp(v, OP_Close, 0, 0); sqlite3VdbeAddOp(v, OP_Destroy, pIndex->tnum, pIndex->iDb); sqlite3EndWriteOperation(pParse); } /* Delete the in-memory description of this index. */ |
︙ | ︙ | |||
2529 2530 2531 2532 2533 2534 2535 | ** a read-transaction for all named database files. ** ** It is important that all schema cookies be verified and all ** read transactions be started before anything else happens in ** the VDBE program. But this routine can be called after much other ** code has been generated. So here is what we do: ** | | | > > > > < < < | | > > > > | | | | > | 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565 2566 2567 2568 | ** a read-transaction for all named database files. ** ** It is important that all schema cookies be verified and all ** read transactions be started before anything else happens in ** the VDBE program. But this routine can be called after much other ** code has been generated. So here is what we do: ** ** The first time this routine is called, we code an OP_Goto that ** will jump to a subroutine at the end of the program. Then we ** record every database that needs its schema verified in the ** pParse->cookieMask field. Later, after all other code has been ** generated, the subroutine that does the cookie verifications and ** starts the transactions will be coded and the OP_Goto P2 value ** will be made to point to that subroutine. The generation of the ** cookie verification subroutine code happens in sqlite3FinishCoding(). ** ** If iDb<0 then code the OP_Goto only - don't set flag to verify the ** schema on any databases. This can be used to position the OP_Goto ** early in the code, before we know if any database tables will be used. */ void sqlite3CodeVerifySchema(Parse *pParse, int iDb){ sqlite *db; Vdbe *v; int mask; v = sqlite3GetVdbe(pParse); if( v==0 ) return; /* This only happens if there was a prior error */ db = pParse->db; if( pParse->cookieGoto==0 ){ pParse->cookieGoto = sqlite3VdbeAddOp(v, OP_Goto, 0, 0)+1; } if( iDb>=0 ){ assert( iDb<db->nDb ); assert( db->aDb[iDb].pBt!=0 || iDb==1 ); assert( iDb<32 ); mask = 1<<iDb; if( (pParse->cookieMask & mask)==0 ){ pParse->cookieMask |= mask; pParse->cookieValue[iDb] = db->aDb[iDb].schema_cookie; } } } /* ** Generate VDBE code that prepares for doing an operation that ** might change the database. ** |
︙ | ︙ |
Changes to src/main.c.
︙ | ︙ | |||
10 11 12 13 14 15 16 | ** ************************************************************************* ** Main file for the SQLite library. The routines in this file ** implement the programmer interface to the library. Routines in ** other files are for internal use by SQLite and should not be ** accessed by users of the library. ** | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | ** ************************************************************************* ** Main file for the SQLite library. The routines in this file ** implement the programmer interface to the library. Routines in ** other files are for internal use by SQLite and should not be ** accessed by users of the library. ** ** $Id: main.c,v 1.245 2004/07/19 17:25:25 drh Exp $ */ #include "sqliteInt.h" #include "os.h" #include <ctype.h> /* ** A pointer to this structure is used to communicate information |
︙ | ︙ | |||
48 49 50 51 52 53 54 | /* ** This is the callback routine for the code that initializes the ** database. See sqlite3Init() below for additional information. ** ** Each callback contains the following information: ** | | | | | | 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | /* ** This is the callback routine for the code that initializes the ** database. See sqlite3Init() below for additional information. ** ** Each callback contains the following information: ** ** argv[0] = "table" or "index" or "view" or "trigger" ** argv[1] = name of thing being created ** argv[2] = root page number for table or index. NULL for trigger or view. ** argv[3] = SQL text for the CREATE statement. ** argv[4] = "1" for temporary files, "0" for main database, "2" or more ** for auxiliary database files. ** */ static int sqlite3InitCallback(void *pInit, int argc, char **argv, char **azColName){ InitData *pData = (InitData*)pInit; |
︙ | ︙ | |||
918 919 920 921 922 923 924 | int sqlite3_errcode(sqlite3 *db){ if( !db ) return SQLITE_NOMEM; return db->errCode; } /* | | < | 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 | int sqlite3_errcode(sqlite3 *db){ if( !db ) return SQLITE_NOMEM; return db->errCode; } /* ** Check schema cookies in all databases. If any cookie is out ** of date, return 0. If all schema cookies are current, return 1. */ static int schemaIsValid(sqlite *db){ int iDb; int rc; BtCursor *curTemp; int cookie; int allOk = 1; for(iDb=0; allOk && iDb<db->nDb; iDb++){ Btree *pBt; pBt = db->aDb[iDb].pBt; if( pBt==0 ) continue; rc = sqlite3BtreeCursor(pBt, MASTER_ROOT, 0, 0, 0, &curTemp); if( rc==SQLITE_OK ){ rc = sqlite3BtreeGetMeta(pBt, 1, (u32 *)&cookie); if( rc==SQLITE_OK && cookie!=db->aDb[iDb].schema_cookie ){ allOk = 0; |
︙ | ︙ |
Changes to src/vdbe.c.
︙ | ︙ | |||
39 40 41 42 43 44 45 | ** ** Various scripts scan this source file in order to generate HTML ** documentation, headers files, or other derived files. The formatting ** of the code in this file is, therefore, important. See other comments ** in this file for details. If in doubt, do not deviate from existing ** commenting and indentation practices when changing or adding code. ** | | | 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | ** ** Various scripts scan this source file in order to generate HTML ** documentation, headers files, or other derived files. The formatting ** of the code in this file is, therefore, important. See other comments ** in this file for details. If in doubt, do not deviate from existing ** commenting and indentation practices when changing or adding code. ** ** $Id: vdbe.c,v 1.402 2004/07/19 17:25:25 drh Exp $ */ #include "sqliteInt.h" #include "os.h" #include <ctype.h> #include "vdbeInt.h" /* |
︙ | ︙ | |||
2359 2360 2361 2362 2363 2364 2365 2366 | ** ** Either a transaction needs to have been started or an OP_Open needs ** to be executed (to establish a read lock) before this opcode is ** invoked. */ case OP_VerifyCookie: { int iMeta; assert( pOp->p1>=0 && pOp->p1<db->nDb ); | > > > | > > > > | 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 | ** ** Either a transaction needs to have been started or an OP_Open needs ** to be executed (to establish a read lock) before this opcode is ** invoked. */ case OP_VerifyCookie: { int iMeta; Btree *pBt; assert( pOp->p1>=0 && pOp->p1<db->nDb ); pBt = db->aDb[pOp->p1].pBt; if( pBt ){ rc = sqlite3BtreeGetMeta(pBt, 1, (u32 *)&iMeta); }else{ rc = SQLITE_OK; iMeta = 0; } if( rc==SQLITE_OK && iMeta!=pOp->p2 ){ sqlite3SetString(&p->zErrMsg, "database schema has changed", (char*)0); rc = SQLITE_SCHEMA; } break; } |
︙ | ︙ |
Changes to src/where.c.
︙ | ︙ | |||
8 9 10 11 12 13 14 | ** May you find forgiveness for yourself and forgive others. ** May you share freely, never taking more than you give. ** ************************************************************************* ** This module contains C code that generates VDBE code used to process ** the WHERE clause of SQL statements. ** | | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | ** May you find forgiveness for yourself and forgive others. ** May you share freely, never taking more than you give. ** ************************************************************************* ** This module contains C code that generates VDBE code used to process ** the WHERE clause of SQL statements. ** ** $Id: where.c,v 1.109 2004/07/19 17:25:25 drh Exp $ */ #include "sqliteInt.h" /* ** The query generator uses an array of instances of this structure to ** help it analyze the subexpressions of the WHERE clause. Each WHERE ** clause subexpression is separated from the others by an AND operator. |
︙ | ︙ | |||
691 692 693 694 695 696 697 | pWInfo->a[0].bRev = bRev; *ppOrderBy = 0; } } /* Open all tables in the pTabList and all indices used by those tables. */ | | | 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 | pWInfo->a[0].bRev = bRev; *ppOrderBy = 0; } } /* Open all tables in the pTabList and all indices used by those tables. */ sqlite3CodeVerifySchema(pParse, -1); /* Insert the cookie verifier Goto */ for(i=0; i<pTabList->nSrc; i++){ Table *pTab; Index *pIx; pTab = pTabList->a[i].pTab; if( pTab->isTransient || pTab->pSelect ) continue; sqlite3VdbeAddOp(v, OP_Integer, pTab->iDb, 0); |
︙ | ︙ |
Changes to test/attach2.test.
︙ | ︙ | |||
8 9 10 11 12 13 14 | # May you share freely, never taking more than you give. # #*********************************************************************** # This file implements regression tests for SQLite library. The # focus of this script is testing the ATTACH and DETACH commands # and related functionality. # | | | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | # May you share freely, never taking more than you give. # #*********************************************************************** # This file implements regression tests for SQLite library. The # focus of this script is testing the ATTACH and DETACH commands # and related functionality. # # $Id: attach2.test,v 1.22 2004/07/19 17:25:25 drh Exp $ # set testdir [file dirname $argv0] source $testdir/tester.tcl # Ticket #354 |
︙ | ︙ | |||
173 174 175 176 177 178 179 | execsql {BEGIN} execsql {SELECT * FROM t1} # Lock status: # db - shared(main) # db2 - } {} | | | | | | | 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 | execsql {BEGIN} execsql {SELECT * FROM t1} # Lock status: # db - shared(main) # db2 - } {} lock_status 4.2.1 db {main shared temp unlocked file2 unlocked} lock_status 4.2.2 db2 {main unlocked temp unlocked file2 unlocked} do_test attach2-4.3 { # The read lock held by db does not prevent db2 from reading test.db execsql {SELECT * FROM t1} db2 } {} lock_status 4.3.1 db {main shared temp unlocked file2 unlocked} lock_status 4.3.2 db2 {main unlocked temp unlocked file2 unlocked} do_test attach2-4.4 { # db is holding a read lock on test.db, so we should not be able # to commit a write to test.db from db2 catchsql { INSERT INTO t1 VALUES(1, 2) } db2 } {1 {database is locked}} lock_status 4.4.1 db {main shared temp unlocked file2 unlocked} lock_status 4.4.2 db2 {main unlocked temp unlocked file2 unlocked} do_test attach2-4.5 { # Handle 'db2' reserves file2. execsql {BEGIN} db2 execsql {INSERT INTO file2.t1 VALUES(1, 2)} db2 # Lock status: # db - shared(main) # db2 - reserved(file2) } {} lock_status 4.5.1 db {main shared temp unlocked file2 unlocked} lock_status 4.5.2 db2 {main unlocked temp reserved file2 reserved} do_test attach2-4.6.1 { # Reads are allowed against a reserved database. catchsql { SELECT * FROM file2.t1; } # Lock status: # db - shared(main), shared(file2) # db2 - reserved(file2) } {0 {}} lock_status 4.6.1.1 db {main shared temp unlocked file2 shared} lock_status 4.6.1.2 db2 {main unlocked temp reserved file2 reserved} do_test attach2-4.6.2 { # Writes against a reserved database are not allowed. catchsql { UPDATE file2.t1 SET a=0; } |
︙ | ︙ |
Changes to test/join3.test.
︙ | ︙ | |||
9 10 11 12 13 14 15 | # #*********************************************************************** # This file implements regression tests for SQLite library. # # This file implements tests for joins, including outer joins, where # there are a large number of tables involved in the join. # | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | # #*********************************************************************** # This file implements regression tests for SQLite library. # # This file implements tests for joins, including outer joins, where # there are a large number of tables involved in the join. # # $Id: join3.test,v 1.2 2004/07/19 17:25:25 drh Exp $ set testdir [file dirname $argv0] source $testdir/tester.tcl # An unrestricted join # set result {} |
︙ | ︙ | |||
41 42 43 44 45 46 47 | set sql "SELECT * FROM t1" for {set i 2} {$i<=$N} {incr i} {append sql ", t$i"} set sep WHERE for {set i 1} {$i<$N} {incr i} { append sql " $sep t[expr {$i+1}].x==t$i.x+1" set sep AND } | < < < < < < | 41 42 43 44 45 46 47 48 49 50 51 52 | set sql "SELECT * FROM t1" for {set i 2} {$i<=$N} {incr i} {append sql ", t$i"} set sep WHERE for {set i 1} {$i<$N} {incr i} { append sql " $sep t[expr {$i+1}].x==t$i.x+1" set sep AND } execsql $sql } $result } finish_test |
Changes to test/misc4.test.
︙ | ︙ | |||
9 10 11 12 13 14 15 | # #*********************************************************************** # This file implements regression tests for SQLite library. # # This file implements tests for miscellanous features that were # left out of other test files. # | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | # #*********************************************************************** # This file implements regression tests for SQLite library. # # This file implements tests for miscellanous features that were # left out of other test files. # # $Id: misc4.test,v 1.3 2004/07/19 17:25:25 drh Exp $ set testdir [file dirname $argv0] source $testdir/tester.tcl # Prepare a statement that will create a temporary table. Then do # a rollback. Then try to execute the prepared statement. # |
︙ | ︙ | |||
37 38 39 40 41 42 43 | ROLLBACK; } sqlite3_step $stmt execsql { SELECT * FROM temp.t2; } } {1} | > > > > > > > > > > | > | 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | ROLLBACK; } sqlite3_step $stmt execsql { SELECT * FROM temp.t2; } } {1} # Drop the temporary table, then rerun the prepared statement to # recreate it again. This recreates ticket #807. # do_test misc4-1.2 { execsql {DROP TABLE t2} sqlite3_reset $stmt sqlite3_step $stmt } {SQLITE_ERROR} do_test misc4-1.3 { sqlite3_finalize $stmt } {SQLITE_SCHEMA} finish_test |
Changes to test/tclsqlite.test.
︙ | ︙ | |||
11 12 13 14 15 16 17 | # This file implements regression tests for TCL interface to the # SQLite library. # # Actually, all tests are based on the TCL interface, so the main # interface is pretty well tested. This file contains some addition # tests for fringe issues that the main test suite does not cover. # | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | # This file implements regression tests for TCL interface to the # SQLite library. # # Actually, all tests are based on the TCL interface, so the main # interface is pretty well tested. This file contains some addition # tests for fringe issues that the main test suite does not cover. # # $Id: tclsqlite.test,v 1.27 2004/07/19 17:25:25 drh Exp $ set testdir [file dirname $argv0] source $testdir/tester.tcl # Check the error messages generated by tclsqlite # if {[sqlite3 -has-codec]} { |
︙ | ︙ | |||
129 130 131 132 133 134 135 136 137 138 139 140 141 142 | do_test tcl-1.19 { set v [catch {db total_changes xyz} msg] lappend v $msg } {1 {wrong # args: should be "db total_changes "}} if {[sqlite3 -tcl-uses-utf]} { do_test tcl-2.1 { execsql "CREATE TABLE t\u0123x(a int, b\u1235 float)" execsql "PRAGMA table_info(t\u0123x)" } "0 a int 0 {} 0 1 b\u1235 float 0 {} 0" do_test tcl-2.2 { execsql "INSERT INTO t\u0123x VALUES(1,2.3)" db eval "SELECT * FROM t\u0123x" result break | > | 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | do_test tcl-1.19 { set v [catch {db total_changes xyz} msg] lappend v $msg } {1 {wrong # args: should be "db total_changes "}} if {[sqlite3 -tcl-uses-utf]} { catch {unset ::result} do_test tcl-2.1 { execsql "CREATE TABLE t\u0123x(a int, b\u1235 float)" execsql "PRAGMA table_info(t\u0123x)" } "0 a int 0 {} 0 1 b\u1235 float 0 {} 0" do_test tcl-2.2 { execsql "INSERT INTO t\u0123x VALUES(1,2.3)" db eval "SELECT * FROM t\u0123x" result break |
︙ | ︙ |