diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..688ef23 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.st linguist-language=Smalltalk \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2480da4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +unit_tests.db +CHANGELOG.template +unit_tests.db \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index aeba7a8..350b4ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,41 @@ # CHANGELOG.md -# 1.13 (2025-01-29) +# 1.19 (2026-04-11) -* fix: Use authorInitials in ODBCConnection >> workstationId -* fix: add requirement for Network-kernel package -* Improvement of the documentation +## Added -# 1.12 (2025-01-09) +* `ODBCConnection class >> dsn:` for a data source that does not require authentication. +* Optional `Tests-ODBC` package for unit tests. +* Documentation about unit tests. +* The project's language is set to `Smalltalk` on GitHub. +* A logo for the project :) -* fix: ODBCStatement >> Execute: now supports SQL request with Unicode characters, -* fix: ODBCColumn >> stringFromBuffer now supports SQL columns with Unicode Characters. +## Fixed + +* `SQLInteger` class uses 32-bit values instead of 64-bit values (https://www.unixodbc.org/doc/ODBC64.html). +* Incorrect `TimeStamp` class in `ODBColumn >> dateTimeData` (replaced by the `DateTime` class). +* An argument of a parameterized query can now contain a string. +* Support of UTF8 strings in ODBCStatement >> fillArg:with: +* Improvement of the documentation for the SQLite data source. + +## Removed + +* Unnecessary and broken `ODBCResultTable` class. + +# 1.13 (2026-03-15) + +## Changed + +* Improvement of the documentation. +* Requirement for `Network-kernel package`. + +## Fixed + +* Use `authorInitials` in `ODBCConnection >> workstationId`. + +# 1.12 (2026-01-09) + +## Fixed + +* `ODBCStatement >> Execute:` now supports SQL request with Unicode characters. +* `ODBCColumn >> stringFromBuffer` now supports SQL columns with Unicode Characters. diff --git a/ODBC.pck.st b/ODBC.pck.st index 35027c1..2a76ec4 100644 --- a/ODBC.pck.st +++ b/ODBC.pck.st @@ -1,6 +1,6 @@ -'From Cuis7.6 [latest update: #7777] on 31 January 2026 at 5:43:19 pm'! +'From Cuis7.6 [latest update: #7777] on 9 April 2026 at 9:22:04 pm'! 'Description '! -!provides: 'ODBC' 1 13! +!provides: 'ODBC' 1 19! !requires: 'Network-Kernel' 1 12 nil! !requires: 'FFI' 1 40 nil! SystemOrganization addCategory: #'ODBC-Constants'! @@ -158,16 +158,6 @@ ExternalStructure subclass: #SQLUInteger SQLUInteger class instanceVariableNames: ''! -!classDefinition: #ODBCResultTable category: #'ODBC-Core'! -OrderedCollection subclass: #ODBCResultTable - instanceVariableNames: 'columns preferredColumnWidths extraLinkBlock extraLinkTitle columnPrintBlock' - classVariableNames: '' - poolDictionaries: '' - category: 'ODBC-Core'! -!classDefinition: 'ODBCResultTable class' category: #'ODBC-Core'! -ODBCResultTable class - instanceVariableNames: ''! - !classDefinition: #ODBCRow category: #'ODBC-Core'! IdentityDictionary subclass: #ODBCRow instanceVariableNames: '' @@ -871,24 +861,24 @@ initialize " Initialize the class " self defineFields.! ! -!SQLInteger methodsFor: 'accessing' stamp: ''! +!SQLInteger methodsFor: 'accessing' stamp: 'OA 23/Mar/2026 18:10:40'! value "This method was automatically generated. See SQLInteger class>>fields." - ^handle int64At: 1! ! + ^handle int32At: 1! ! -!SQLInteger methodsFor: 'accessing' stamp: ''! +!SQLInteger methodsFor: 'accessing' stamp: 'OA 23/Mar/2026 18:11:07'! value: anObject "This method was automatically generated. See SQLInteger class>>fields." - handle int64At: 1 put: anObject! ! + handle int32At: 1 put: anObject! ! -!SQLInteger class methodsFor: 'accessing' stamp: 'jfr 14/Nov/2023 17:01:22'! +!SQLInteger class methodsFor: 'accessing' stamp: 'OA 23/Mar/2026 18:11:42'! fields " SQLInteger defineFields " - ^ #(#(#value 'int3264') )! ! + ^ #(#(#value 'int32') )! ! !SQLInteger class methodsFor: 'accessing' stamp: 'jfr 14/Nov/2023 16:59:24'! initialize @@ -1114,196 +1104,6 @@ initialize " Initialize the class " self defineFields.! ! -!ODBCResultTable methodsFor: 'adding' stamp: 'rjl 4/Sep/2008 16:06:00'! -add: row - self maxWidthOfColumnsForRow: row. - ^ super add: row! ! - -!ODBCResultTable methodsFor: 'converting' stamp: 'rjl 4/Sep/2008 16:07:00'! -asMorph - | twoWayScroller report title window | - report := TextMorph new - backgroundColor: Color transparent; - borderWidth: 0; - margins: 6; - beAllFont: (StrikeFont - familyName: #BitstreamVeraSansMono - size: 12); - contents: (Text streamContents: [ :stream | self printTextOn: stream ]). - twoWayScroller := TwoWayScrollPane new - borderWidth: 0; - setScrollDeltas. - twoWayScroller scroller addMorph: report. - title := String streamContents: - [ :stream | - stream - nextPutAll: 'Query Results ('; - nextPutAll: self size asString , ' row'. - self size ~= 1 ifTrue: [ stream nextPut: $s ]. - stream nextPut: $) ]. - window := (SystemWindow labelled: title) paneColor: (Color - r: 1.0 - g: 0.903 - b: 0.258). - window extent: 700 @ 400. - window position: (Display extent - window extent) // 2. - ^ window - addMorph: twoWayScroller - fullFrame: (LayoutFrame fractions: (0 @ 0 extent: 1 @ 1))! ! - -!ODBCResultTable methodsFor: 'converting' stamp: 'rjl 4/Sep/2008 16:07:00'! -openAsMorph - self asMorph openAsIsIn: ActiveWorld! ! - -!ODBCResultTable methodsFor: 'accessing' stamp: 'rjl 4/Sep/2008 16:07:00'! -columnNames - ^ self columns collect: [ :each | each name ]! ! - -!ODBCResultTable methodsFor: 'accessing' stamp: 'rjl 4/Sep/2008 16:07:00'! -columnPrintBlock - ^ columnPrintBlock ifNil: [ self standardColumnPrintBlock ]! ! - -!ODBCResultTable methodsFor: 'accessing' stamp: 'rjl 4/Sep/2008 16:07:00'! -columnPrintBlock: aThreeArgBlock - columnPrintBlock := aThreeArgBlock! ! - -!ODBCResultTable methodsFor: 'accessing' stamp: 'rjl 4/Sep/2008 16:07:00'! -columns - ^ columns! ! - -!ODBCResultTable methodsFor: 'accessing' stamp: 'rjl 4/Sep/2008 16:07:00'! -columns: aList - columns := aList reject: - [ :each | - #( - #DBConnect - #DBName - #DatabaseName - #ImportVersion - #Locked - ) includes: each name ]! ! - -!ODBCResultTable methodsFor: 'accessing' stamp: 'rjl 4/Sep/2008 16:07:00'! -extraLinkTitle: aString do: aOneArgumentBlock - extraLinkTitle := aString. - extraLinkBlock := aOneArgumentBlock! ! - -!ODBCResultTable methodsFor: 'accessing' stamp: 'rjl 4/Sep/2008 16:07:00'! -maxWidthOfColumn: anODBCColumn - ^ (preferredColumnWidths at: anODBCColumn) + 2! ! - -!ODBCResultTable methodsFor: 'accessing' stamp: 'rjl 4/Sep/2008 16:07:00'! -maxWidthOfColumnsForRow: row - self columns do: - [ :each | - | currentWidth | - currentWidth := preferredColumnWidths - at: each - ifAbsentPut: [ each name size ]. - preferredColumnWidths - at: each - put: (currentWidth max: (row at: each name) printString size) ]! ! - -!ODBCResultTable methodsFor: 'accessing' stamp: 'rjl 4/Sep/2008 16:07:00'! -preferredColumnWidths - "Return a list of associations so that column order is preserved" - ^ self columns collect: [ :each | each -> (self maxWidthOfColumn: each) ]! ! - -!ODBCResultTable methodsFor: 'accessing' stamp: 'rjl 4/Sep/2008 16:09:00'! -standardColumnPrintBlock - ^ - [ :row :column :textStream | - | columnValue | - columnValue := row at: column key name. - columnValue isFraction ifTrue: [ columnValue := columnValue asFloat ]. - textStream nextPutAll: (self class - formatItem: columnValue asString - toWidth: column value) ]! ! - -!ODBCResultTable methodsFor: 'initialization' stamp: 'rjl 4/Sep/2008 16:07:00'! -initialize - preferredColumnWidths := Dictionary new! ! - -!ODBCResultTable methodsFor: 'printing' stamp: 'rjl 4/Sep/2008 16:07:00'! -printHeaderOn: aStream - self columns do: - [ :each | - | columnHeader | - columnHeader := self class - formatItem: each name - toWidth: (self maxWidthOfColumn: each). - aStream nextPutAll: columnHeader ]. - aStream cr. - self columns do: - [ :each | - aStream - nextPutAll: (String - new: (self maxWidthOfColumn: each) - 1 - withAll: $-); - nextPut: $ ]. - aStream cr! ! - -!ODBCResultTable methodsFor: 'printing' stamp: 'rjl 4/Sep/2008 16:07:00'! -printOn: aStream - self printHeaderOn: aStream. - self do: - [ :each | - each - printOn: aStream - withColumnDefinitions: self preferredColumnWidths. - aStream cr ]! ! - -!ODBCResultTable methodsFor: 'printing' stamp: 'rjl 4/Sep/2008 16:09:00'! -printTextForRow: aRow on: aTextStream - self preferredColumnWidths do: - [ :each | - self columnPrintBlock - value: aRow - value: each - value: aTextStream ]. - extraLinkTitle ifNotNil: - [ aTextStream - withAttribute: (PluggableTextAttribute evalBlock: [ extraLinkBlock value: aRow ]) - do: [ aTextStream nextPutAll: extraLinkTitle ] ]. - aTextStream - space; - cr! ! - -!ODBCResultTable methodsFor: 'printing' stamp: 'rjl 4/Sep/2008 16:09:00'! -printTextOn: aTextStream - self printHeaderOn: aTextStream. - self do: - [ :each | - self - printTextForRow: each - on: aTextStream ]! ! - -!ODBCResultTable methodsFor: 'private' stamp: 'rjl 4/Sep/2008 16:09:00'! -species - ^ OrderedCollection! ! - -!ODBCResultTable class methodsFor: 'formatting' stamp: 'bvs 14/Apr/2004 14:15:00'! -formatItem: aString toWidth: aNumber - - ^ aString - padded: #right - to: aNumber - with: $ ! ! - -!ODBCResultTable class methodsFor: 'instance creation' stamp: 'jrp 10/Mar/2004 13:23:00'! -new - - ^ super new initialize! ! - -!ODBCResultTable class methodsFor: 'instance creation' stamp: 'rjl 4/Sep/2008 16:06:00'! -newFrom: anODBCResultSet - | table | - table := self new. - table columns: anODBCResultSet columns. - anODBCResultSet do: [ :each | table add: each ]. - anODBCResultSet close. - ^ table! ! - !ODBCRow methodsFor: 'error handling' stamp: 'rjl 4/Sep/2008 15:25:00'! doesNotUnderstand: aMessage | originalSelector | @@ -1319,18 +1119,6 @@ doesNotUnderstand: aMessage ifPresent: [ :val | ^ val ] ]. ^ super doesNotUnderstand: aMessage! ! -!ODBCRow methodsFor: 'printing' stamp: 'rjl 4/Sep/2008 16:00:00'! -printOn: aStream withColumnDefinitions: aList - aList do: - [ :each | - aStream nextPutAll: (ODBCResultTable - formatItem: (self at: each key name) asString - toWidth: each value) ]! ! - -!ODBCResultSet methodsFor: 'converting' stamp: 'rjl 4/Sep/2008 15:59:00'! -asTable - ^ ODBCResultTable newFrom: self! ! - !ODBCResultSet methodsFor: 'testing' stamp: 'dgd 27/May/2002 23:14:00'! atEnd self checkConnected. @@ -1403,7 +1191,7 @@ connection contents ^ self shouldNotImplement! ! -!ODBCResultSet methodsFor: 'accessing' stamp: 'jfr 13/Nov/2023 16:02:24'! +!ODBCResultSet methodsFor: 'accessing' stamp: 'OA 22/Mar/2026 18:23:11'! fetchRow "private - fetch the next row" | row ret | @@ -1462,7 +1250,7 @@ unregisterForFinalization finalization notification" connection class unregister: self! ! -!ODBCResultSet methodsFor: 'initialization' stamp: 'jfr 13/Nov/2023 16:02:30'! +!ODBCResultSet methodsFor: 'initialization' stamp: 'OA 23/Mar/2026 16:24:46'! initializeConnection: aConnection statement: aStatement "initialize the receiver" | columnCount | @@ -1606,7 +1394,7 @@ initialize digits := 0. size := 0.! ! -!ODBCColumn methodsFor: 'private - type convertion' stamp: 'RMV 30/Oct/2024 20:54:58'! +!ODBCColumn methodsFor: 'private - type convertion' stamp: 'OA 23/Mar/2026 17:47:20'! asNumber: aString "creates a Number from aString. Could be an Integer or a Fraction" | stream zero sign integerPart char fractionPart scale | @@ -1645,10 +1433,10 @@ dateData month: buffer month year: buffer year! ! -!ODBCColumn methodsFor: 'private - type convertion' stamp: 'ar 2/Aug/2008 14:40:00'! +!ODBCColumn methodsFor: 'private - type convertion' stamp: 'OA 25/Mar/2026 17:18:06'! dateTimeData "answer the data for this column in the current row as a Date/Time" - ^TimeStamp + ^DateAndTime year: buffer year month: buffer month day: buffer day @@ -1657,7 +1445,7 @@ dateTimeData second: buffer second ! ! -!ODBCColumn methodsFor: 'private - type convertion' stamp: 'dgd 31/May/2002 22:05:00'! +!ODBCColumn methodsFor: 'private - type convertion' stamp: 'OA 23/Mar/2026 17:14:40'! doubleData "answer the data for this column in the current row as an Double" ^ buffer value! ! @@ -1667,10 +1455,9 @@ floatData "answer the data for this column in the current row as an Float" ^ buffer value! ! -!ODBCColumn methodsFor: 'private - type convertion' stamp: 'dgd 31/May/2002 22:05:00'! +!ODBCColumn methodsFor: 'private - type convertion' stamp: 'OA 23/Mar/2026 17:14:45'! integerData "answer the data for this column in the current row as an Integer" - ^ buffer value! ! !ODBCColumn methodsFor: 'private - type convertion' stamp: 'dgd 2/Jun/2002 00:59:00'! @@ -1708,7 +1495,7 @@ timeData ^ Time fromSeconds: buffer hour * 3600 + (buffer minute * 60) + buffer second! ! -!ODBCColumn methodsFor: 'initialization' stamp: 'RMV 30/Oct/2024 20:57:35'! +!ODBCColumn methodsFor: 'initialization' stamp: 'OA 23/Mar/2026 16:08:40'! bindBuffer "bind the column's buffer" | bufferSize bufferHandle | @@ -1743,15 +1530,15 @@ free buffer notNil ifTrue: [buffer free]! ! -!ODBCColumn methodsFor: 'initialization' stamp: 'dgd 2/Jun/2002 20:20:00'! +!ODBCColumn methodsFor: 'initialization' stamp: 'OA 23/Mar/2026 18:03:28'! initializeDataType: anInteger dataType := self class dataTypeNameFor: anInteger. convertSelector := self class convertBufferSelectorFor: anInteger. initializeSelector := self class initializeBufferSelectorFor: anInteger. - + cType := self class cTypeFor: anInteger! ! -!ODBCColumn methodsFor: 'initialization' stamp: 'RMV 30/Oct/2024 20:58:00'! +!ODBCColumn methodsFor: 'initialization' stamp: 'OA 23/Mar/2026 17:02:51'! initializeResultSet: aResultSet number: anInteger "initialize the receiver" | columnName nameLenght columDataType columnSize decimalDigits columnNullable | @@ -1898,7 +1685,7 @@ convertBufferSelectorFor: anInteger ifPresent: [:pair | ^ pair second]. ^ #stringData! ! -!ODBCColumn class methodsFor: 'data types' stamp: 'dgd 31/May/2002 21:31:00'! +!ODBCColumn class methodsFor: 'data types' stamp: 'OA 23/Mar/2026 17:11:06'! dataTypeNameFor: anInteger "answer the datatype name for anInteger" DataTypes @@ -2290,10 +2077,6 @@ resultSetFor: aString secondTry := true. err retry ]! ! -!ODBCConnection methodsFor: 'statements' stamp: 'rjl 4/Sep/2008 16:03:00'! -run: aString - ^ (self resultSetFor: aString) asTable! ! - !ODBCConnection methodsFor: 'private - finalization' stamp: 'dgd 27/May/2002 20:55:00'! finalize self closeNotFail! ! @@ -2469,6 +2252,17 @@ closeAll do: [:each | each close]. self registry finalizeValues! ! +!ODBCConnection class methodsFor: 'instance creation' stamp: 'OA 22/Mar/2026 15:24:54'! +dsn: dsnString + "creates a new instance of the receiver and open the connection without authentication" + | instance | + instance := self new + initializeDsn: dsnString + user: '' + password: ''. + [instance open] on: ODBCWarning do: [:e | e resume ]. + ^ instance! ! + !ODBCConnection class methodsFor: 'instance creation' stamp: 'rjl 4/Sep/2008 15:33:00'! dsn: dsnString user: userString password: passwordString "creates a new instance of the receiver and open the connection" @@ -2694,15 +2488,15 @@ bind: arguments ]. ! ! -!ODBCStatement methodsFor: 'executing' stamp: 'fgz 2/Jul/2024 09:36:04'! +!ODBCStatement methodsFor: 'executing' stamp: 'OA 29/Mar/2026 21:52:53'! bindArg: arg "Bind the argument at the given position" | buf sz | "String arguments" arg isString ifTrue:[ - sz := arg size. + sz := arg utf8BytesSize. buf := ExternalAddress allocate: sz. - 1 to: sz do:[:b| buf unsignedByteAt: b put: (arg uint8At: b)]. + 1 to: sz do:[:b| buf unsignedByteAt: b put: (arg asUtf8Bytes at: b)]. ^(ODBCBoundParameter new) handle: buf; cType: SQLCCHAR; sqlType: SQLVARCHAR; colWidth: sz; size: sz]. @@ -2771,7 +2565,7 @@ execute: args ^ODBCResultSet connection: connection statement: self! ! -!ODBCStatement methodsFor: 'executing' stamp: 'fgz 2/Jul/2024 09:36:14'! +!ODBCStatement methodsFor: 'executing' stamp: 'OA 1/Apr/2026 20:34:24'! fillArg: buf with: arg "Fill a bound parameter with a new value. Answer true if successful, false otherwise" | argHandle | @@ -2781,8 +2575,8 @@ fillArg: buf with: arg buf sqlType caseOf: { [SQLVARCHAR] -> [ arg isString ifFalse:[^false]. - arg size > buf size ifTrue:[^false]. - 1 to: arg size do:[:b| argHandle unsignedByteAt: b put: (arg uint8At: b)]. + arg utf8BytesSize > buf size ifTrue:[^false]. + 1 to: arg size do:[:b| argHandle unsignedByteAt: b put: (arg asByteArray at: b)]. ^true ]. [SQLINTEGER] -> [ diff --git a/README.md b/README.md index 8478f86..db821ef 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +![DatabaseSupport](assets/DatabaseSupport.png) + # DatabaseSupport The DatabaseSupport package provides an [ODBC](https://en.wikipedia.org/wiki/Open_Database_Connectivity) library for Cuis Smalltalk. @@ -74,7 +76,7 @@ brew install unixodbc With [Homebrew](https://brew.sh), libraries are installed in the `/opt/homebrew/lib` directory. ## Database-specific Drivers -Most databases provide an ODBC driver. Below you’ll find the installation procedure for the major open-source relational databases. For more specific products such as [Oracle](https://www.oracle.com/database/technologies/releasenote-odbc-ic.html) or [IBM Db2](https://www.ibm.com/docs/en/db2-warehouse?topic=db2-downloading-clients-drivers), we recommend consulting the official documentation. +Most databases provide an [ODBC driver](https://www.unixodbc.org/drivers.html). Below you’ll find the installation procedure for the major open-source relational databases. For more specific products such as [Oracle](https://www.oracle.com/database/technologies/releasenote-odbc-ic.html) or [IBM Db2](https://www.ibm.com/docs/en/db2-warehouse?topic=db2-downloading-clients-drivers), we recommend consulting the official documentation. Download a database-specific driver for each kind of database being used. The installation method varies depending on the operating system used. @@ -159,7 +161,7 @@ Create a `odbc.ini` file with contents similar to the following, which defines d [TodosDSN] Description = SQLite database for a Todo app - Driver = /opt/homebrew/lib/libsqlite3odbc.so + Driver = SQLite Database = /Users/volkmannm/Documents/dev/lang/smalltalk/Cuis-Smalltalk-Dev-UserFiles/todos.db The file `odbcinst.ini` associates driver names with paths to their shared libraries. In the `odbc.ini` file above, the `Driver` values is the absolute path to the driver shared library. But using driver names specfied in the `odbcinst.ini` file avoids needing to repeat the shared library paths for each data source that uses the same driver. @@ -208,10 +210,36 @@ This script also starts a Smalltalk VM using the base image. To use another imag ### Install the DatabaseConnection package Open an "Installed Packages" window and verify that the ODBC package is installed. If not, open a Workspace, enter `Feature require: 'ODBC'`, and "Do it". -### Code snippets +## How to use unit tests ? + +This section describes using unit tests to verify the proper functioning of the `DatabaseSupport` package. The instructions below are for Cuis‑Smalltalk in a Linux environment. If you are working on macOS, you must adapt the steps. + +If not present, you must install the ODBC driver for SQLite databases. This step differs depending on your operating system. On Linux, use the `apt` command: + + # sudo apt install libsqliteodbc + +Create a SQLite database in the folder containing the `DatabaseSupport` package. This database must be named `unit_tests.db`. + + # sqlite3 unit_tests.db + +If the ODBC driver for SQLite is not declared in the `odbcinst.ini` file, add the following section : + + [SQLite] + Description=SQLite ODBC Driver + Driver=libsqlite3odbc.so + UsageCount=1 + +Now declare the data source in the `odbc.ini` file. You must adjust the database path to match where you created it. The configuration file shown here is for Linux. + + [UnitTestsDSN] + Description = SQLite test database for the DatabaseSupport package + Driver = SQLite + Database = /home/olivier/unit_tests.db + +## Code snippets These code snippets are practical and common examples for designing applications that use a relational database. -#### A SQL query +### A SQL query ```smalltalk conn := ODBCConnection dsn: 'TodosDSN' user: 'user' password: '1234'. stmt := conn query: 'select * from todos'. @@ -222,7 +250,7 @@ rs do: [:row | row print]. conn close. ``` -#### Get the list of columns of a table +### Get the list of columns of a table This snippet is a practical example to extract metadata from a SQL object. ```smalltalk @@ -237,7 +265,7 @@ columns do: [:column | column name print]. conn close. ``` -#### Parameterized SQL query +### Parameterized SQL query Using parameters makes it easier to construct the query by avoiding string concatenation, which can make the code difficult to read and complex to evolve. ```smalltalk @@ -250,7 +278,7 @@ rs do: [:row | row print]. conn close. ``` -#### Prepared Statements +### Prepared Statements A prepared statement is compiled by the database server. Its execution will be faster if it is reused. ```smalltalk @@ -264,7 +292,7 @@ rs do: [:row | row print]. conn close. ``` -#### A prepared statement with parameters +### A prepared statement with parameters It is recommended to use prepared statements built with parameters to prevent [SQL injections](https://www.w3schools.com/sql/sql_injection.asp). ```smalltalk @@ -278,7 +306,7 @@ rs do: [:row | row print]. conn close. ``` -#### Transactions +### Transactions A transaction is a sequence of one or more operations that are treated as a single unit of work. Transactions ensure that all operations within the block are completed successfully; if any part fails, the transaction can be rolled back, leaving the system in a consistent state. Transactions are typically used in databases to maintain data integrity and consistency. A transaction block starts with `beginTransaction`. Use `commitTransaction` to validate a transaction or `rollbackTransaction` to cancel one. @@ -294,4 +322,3 @@ conn commitTransaction. conn close. ``` - diff --git a/Tests-ODBC.pck.st b/Tests-ODBC.pck.st new file mode 100644 index 0000000..77473da --- /dev/null +++ b/Tests-ODBC.pck.st @@ -0,0 +1,239 @@ +'From Cuis7.6 [latest update: #7777] on 11 April 2026 at 6:40:00 pm'! +'Description '! +!provides: 'Tests-ODBC' 1 5! +!requires: 'ODBC' 1 15 nil! +SystemOrganization addCategory: #'Tests-ODBC'! + + +!classDefinition: #ODBCTests category: #'Tests-ODBC'! +TestCase subclass: #ODBCTests + instanceVariableNames: 'connection' + classVariableNames: '' + poolDictionaries: '' + category: 'Tests-ODBC'! +!classDefinition: 'ODBCTests class' category: #'Tests-ODBC'! +ODBCTests class + instanceVariableNames: ''! + + +!ODBCTests methodsFor: 'setUp/tearDown' stamp: 'OA 29/Mar/2026 21:58:34'! +setUp + | stmt queries | + + queries := OrderedCollection new + add: 'DROP TABLE IF EXISTS users'; + add: 'CREATE TABLE users (id INTEGER PRIMARY KEY, login TEXT NOT NULL, password TEXT, email TEXT UNIQUE, value INTEGER, score REAL, admin BOOLEAN, modified TEXT);'; + add: 'INSERT INTO users(login,password,email,value,score,admin,modified) VALUES(''jdoe'',''ééàà1234'',''john.doe@cuis.org'',42,3.14156,TRUE,NULL);'; + add: 'INSERT INTO users(login,password,email,value,score,admin,modified) VALUES(''psmith'',''@1234'',''peter.smith@cuis.org'',26,1.0,FALSE,TRUE);'; + yourself. + + connection := ODBCConnection dsn: 'UnitTestsDSN'. + + queries do: [ :query | + stmt := connection query: query. + stmt execute. + ]! ! + +!ODBCTests methodsFor: 'setUp/tearDown' stamp: 'OA 22/Mar/2026 17:03:31'! +tearDown + | stmt | + + stmt := connection query: 'DROP TABLE IF EXISTS users'. + stmt execute. + + connection close.! ! + +!ODBCTests methodsFor: 'select' stamp: 'OA 9/Apr/2026 19:02:05'! +testSelect + | stmt rs | + + stmt := connection query: 'SELECT * FROM users WHERE id=1'. + rs := (stmt execute) next. + + self assert: (rs isNil) not. + self assert: (rs at: #id) = 1. + self assert: (rs at: #login) = 'jdoe'. + self assert: (rs at: #password) = 'ééàà1234'. + self assert: (rs at: #email) = 'john.doe@cuis.org'. + self assert: (rs at: #value) = 42. + self assert: (rs at: #score) = 3.14156. + self assert: (rs at: #admin) = true. + self assert: (rs at: #modified) = nil. + ! ! + +!ODBCTests methodsFor: 'select' stamp: 'OA 29/Mar/2026 12:00:50'! +testSelectCount + | stmt rs | + + stmt := connection query: 'SELECT COUNT(*) AS nbr FROM users'. + rs := stmt execute. + + self assert: (rs next at: #nbr) = 2.! ! + +!ODBCTests methodsFor: 'select' stamp: 'OA 29/Mar/2026 12:08:16'! +testSelectWhere + | stmt rs | + + stmt := connection query: 'SELECT * FROM users WHERE id = 2'. + rs := (stmt execute) next. + + self assert: (rs at: #login) = 'psmith'. +! ! + +!ODBCTests methodsFor: 'select' stamp: 'OA 29/Mar/2026 12:08:57'! +testSelectWhereOrder + | stmt rs | + + stmt := connection query: 'SELECT * FROM users WHERE id = 2 ORDER BY id DESC'. + rs := (stmt execute) next. + + self assert: (rs at: #id) = 2. +! ! + +!ODBCTests methodsFor: 'select' stamp: 'OA 29/Mar/2026 12:15:44'! +testSelectWherePreparedStatement + | stmt rs | + + stmt := connection prepare: 'SELECT login FROM users WHERE id = 1'. + rs := (stmt execute) next. + + self assert: (rs at: #login) = 'jdoe'. +! ! + +!ODBCTests methodsFor: 'select' stamp: 'OA 29/Mar/2026 22:00:38'! +testSelectWherePreparedStatementWithParameters + | stmt rs | + + stmt := connection prepare: 'SELECT email FROM users WHERE id = ? AND login = ? AND password = ?'. + rs := (stmt execute: #(1 'jdoe' 'ééàà1234')) next. + + self assert: (rs at: #email) = 'john.doe@cuis.org'. +! ! + +!ODBCTests methodsFor: 'delete' stamp: 'OA 9/Apr/2026 21:07:05'! +testDelete + | stmt rs | + + stmt := connection query: 'DELETE FROM users WHERE id=1'. + stmt execute. + + stmt := connection query: 'SELECT COUNT(*) AS nbr FROM users'. + rs := stmt execute. + + self assert: (rs next at: #nbr) = 1.! ! + +!ODBCTests methodsFor: 'delete' stamp: 'OA 9/Apr/2026 21:08:40'! +testDeletePreparedStatementWithParameters + | rs stmt | + + stmt := connection prepare: 'DELETE FROM users WHERE id = ?;'. + + rs := stmt execute: (Array with: 1). + + stmt := connection query: 'SELECT COUNT(*) AS nbr FROM users'. + rs := stmt execute. + + self assert: (rs next at: #nbr) = 1.! ! + +!ODBCTests methodsFor: 'update' stamp: 'OA 9/Apr/2026 19:05:35'! +testUpdate + | stmt rs | + + stmt := connection query: 'UPDATE users SET password=''kkkkkk'', email=''jjdoe@cuis.org'' WHERE id=1'. + stmt execute. + + stmt := connection query: 'SELECT * FROM users WHERE id=1'. + rs := (stmt execute) next. + + self assert: (rs at: #password) = 'kkkkkk'. + self assert: (rs at: #email) = 'jjdoe@cuis.org'. + ! ! + +!ODBCTests methodsFor: 'update' stamp: 'OA 9/Apr/2026 21:09:57'! +testUpdatePreparedStatementWithParameters + | stmt rs | + + stmt := connection prepare: 'UPDATE users SET password=?, email=? WHERE id=?'. + + rs := stmt execute: (Array with: 'kéàçè' with: 'jjdoe@cuis.org' with: 1). + + stmt := connection query: 'SELECT * FROM users WHERE id=1'. + rs := (stmt execute) next. + + self assert: (rs at: #password) = 'kéàçè'. + self assert: (rs at: #email) = 'jjdoe@cuis.org'. + ! ! + +!ODBCTests methodsFor: 'insert' stamp: 'OA 31/Mar/2026 21:45:43'! +testInsert + | stmt rs | + + 1 to: 10 do: [ :i | + | user email | + user := 'user' , i asString. + email := user , '@cuis.org'. + stmt := connection query: 'INSERT INTO users(login,email) VALUES(''',user,''',''',email,''');'. + stmt execute. + ]. + + stmt := connection query: 'SELECT COUNT(*) AS nbr FROM users'. + rs := stmt execute. + + self assert: (rs next at: #nbr) = 12.! ! + +!ODBCTests methodsFor: 'insert' stamp: 'OA 5/Apr/2026 07:20:44'! +testInsertPreparedStatementWithParameters + | rs stmt | + + stmt := connection prepare: 'INSERT INTO users(login,email) VALUES(?,?);'. + + rs := stmt execute: (Array with: 'user1' with: 'user1@cuis.org'). + + stmt := connection query: 'SELECT COUNT(*) AS nbr FROM users'. + rs := stmt execute. + + self assert: (rs next at: #nbr) = 3.! ! + +!ODBCTests methodsFor: 'insert' stamp: 'OA 11/Apr/2026 18:37:45'! +testInsertPreparedStatementsWithParameters + "This test fails and causes the system to become unstable." + + "| rs stmt | + + stmt := connection prepare: 'INSERT INTO users(login,email) VALUES(?,?);'. + + rs := stmt execute: (Array with: 'user1' with: 'user1@cuis.org'). + rs := stmt execute: (Array with: 'user2' with: 'user2@cuis.org'). + + stmt := connection query: 'SELECT COUNT(*) AS nbr FROM users'. + rs := stmt execute. + + self assert: (rs next at: #nbr) = 4."! ! + +!ODBCTests methodsFor: 'insert' stamp: 'OA 11/Apr/2026 18:37:57'! +testLoopInsertPreparedStatementWithParameters + "This test fails and causes the system to become unstable." + + "| stmt rs | + + stmt := connection prepare: 'INSERT INTO users(login,email) VALUES(?,?);'. + + 1 to: 10 do: [ :i | + | user email | + user := 'user' , i asString. + email := user , '@cuis.org'. + stmt execute: (Array with: user with: email). + ]. + + stmt := connection query: 'SELECT COUNT(*) AS nbr FROM users'. + rs := stmt execute. + + self assert: (rs next at: #nbr) = 12."! ! + +!ODBCTests methodsFor: 'accessing' stamp: 'OA 29/Mar/2026 11:46:25'! +connection + ^connection! ! + +!ODBCTests methodsFor: 'accessing' stamp: 'OA 29/Mar/2026 11:46:55'! +connection: anODBCConnection + connection := anODBCConnection ! ! diff --git a/assets/DatabaseSupport.png b/assets/DatabaseSupport.png new file mode 100644 index 0000000..63dc1c5 Binary files /dev/null and b/assets/DatabaseSupport.png differ