From bbc3e50d3a9fe3faab0ef13d2ff0fd0afff0ad35 Mon Sep 17 00:00:00 2001 From: Olivier Auverlot Date: Sat, 21 Mar 2026 06:41:14 +0100 Subject: [PATCH 01/12] remove the unnecessary and broken ODBCResultTable class --- CHANGELOG.md | 4 + ODBC.pck.st | 220 +-------------------------------------------------- 2 files changed, 6 insertions(+), 218 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aeba7a8..eca5a59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG.md +# 1.14 (2025-03-21) + +* cleaning: remove the unnecessary and broken ODBCResultTable class + # 1.13 (2025-01-29) * fix: Use authorInitials in ODBCConnection >> workstationId diff --git a/ODBC.pck.st b/ODBC.pck.st index 35027c1..43c4f34 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 21 March 2026 at 6:40:07 am'! 'Description '! -!provides: 'ODBC' 1 13! +!provides: 'ODBC' 1 14! !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: '' @@ -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. @@ -2290,10 +2078,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! ! From 2cd521b4db07f3f32452ac04008639f06a74587f Mon Sep 17 00:00:00 2001 From: Olivier Auverlot Date: Sun, 22 Mar 2026 08:44:40 +0100 Subject: [PATCH 02/12] Configuration fix for SQLite in documentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8478f86..bcbbfbe 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,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. From 573fcc54831a479300af4da2093a4a7aef6f38d7 Mon Sep 17 00:00:00 2001 From: Olivier Auverlot Date: Mon, 23 Mar 2026 21:46:39 +0100 Subject: [PATCH 03/12] INTEGER type is fixed --- CHANGELOG.md | 6 +++++- ODBC.pck.st | 48 +++++++++++++++++++++++++++++------------------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eca5a59..5de9618 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,11 @@ # CHANGELOG.md -# 1.14 (2025-03-21) +# 1.16 (2025-03-23) +* new: add ODBCConnection class >> dsn: for a data source that does not require authentication +* new: update of the documentation about unit tests +* fix: The INTEGER type is fixed. It was incorrectly defined as 64 bits instead of 32 bits +* fix: improvement of the documentation for the SQLite data source * cleaning: remove the unnecessary and broken ODBCResultTable class # 1.13 (2025-01-29) diff --git a/ODBC.pck.st b/ODBC.pck.st index 43c4f34..b0eab6a 100644 --- a/ODBC.pck.st +++ b/ODBC.pck.st @@ -1,6 +1,6 @@ -'From Cuis7.6 [latest update: #7777] on 21 March 2026 at 6:40:07 am'! +'From Cuis7.6 [latest update: #7777] on 23 March 2026 at 9:37:40 pm'! 'Description '! -!provides: 'ODBC' 1 14! +!provides: 'ODBC' 1 16! !requires: 'Network-Kernel' 1 12 nil! !requires: 'FFI' 1 40 nil! SystemOrganization addCategory: #'ODBC-Constants'! @@ -861,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 @@ -1191,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 | @@ -1250,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 | @@ -1394,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 | @@ -1445,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! ! @@ -1455,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'! @@ -1496,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 | @@ -1531,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 | @@ -1686,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 @@ -2253,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" From 53ccaba4c21904e7a5f7f0ac48ed18c850e70d37 Mon Sep 17 00:00:00 2001 From: Olivier Auverlot Date: Wed, 25 Mar 2026 17:43:42 +0100 Subject: [PATCH 04/12] incorrect TimeStamp class use in ODBColumn >> dateTimeData (replaced by DateTime class) --- CHANGELOG.md | 5 +++-- ODBC.pck.st | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5de9618..6a08d14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,15 @@ # CHANGELOG.md -# 1.16 (2025-03-23) +# 1.17 (2025-03-25) * new: add ODBCConnection class >> dsn: for a data source that does not require authentication * new: update of the documentation about unit tests * fix: The INTEGER type is fixed. It was incorrectly defined as 64 bits instead of 32 bits * fix: improvement of the documentation for the SQLite data source +* fix: incorrect TimeStamp class use in ODBColumn >> dateTimeData (replaced by DateTime class) * cleaning: remove the unnecessary and broken ODBCResultTable class -# 1.13 (2025-01-29) +# 1.13 (2025-03-15) * fix: Use authorInitials in ODBCConnection >> workstationId * fix: add requirement for Network-kernel package diff --git a/ODBC.pck.st b/ODBC.pck.st index b0eab6a..c07afa7 100644 --- a/ODBC.pck.st +++ b/ODBC.pck.st @@ -1,6 +1,6 @@ -'From Cuis7.6 [latest update: #7777] on 23 March 2026 at 9:37:40 pm'! +'From Cuis7.6 [latest update: #7777] on 25 March 2026 at 5:21:48 pm'! 'Description '! -!provides: 'ODBC' 1 16! +!provides: 'ODBC' 1 17! !requires: 'Network-Kernel' 1 12 nil! !requires: 'FFI' 1 40 nil! SystemOrganization addCategory: #'ODBC-Constants'! @@ -1433,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 From 0434f166b608f7e5c222620d9641f0ac88cb7227 Mon Sep 17 00:00:00 2001 From: Olivier Auverlot Date: Wed, 25 Mar 2026 18:01:16 +0100 Subject: [PATCH 05/12] File reformated --- CHANGELOG.md | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a08d14..d1ca999 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,21 +1,37 @@ # CHANGELOG.md -# 1.17 (2025-03-25) +# Unreleased -* new: add ODBCConnection class >> dsn: for a data source that does not require authentication -* new: update of the documentation about unit tests -* fix: The INTEGER type is fixed. It was incorrectly defined as 64 bits instead of 32 bits -* fix: improvement of the documentation for the SQLite data source -* fix: incorrect TimeStamp class use in ODBColumn >> dateTimeData (replaced by DateTime class) -* cleaning: remove the unnecessary and broken ODBCResultTable class +## Added + +* `ODBCConnection class >> dsn:` for a data source that does not require authentication. +* Documentation about unit tests. +* Optional package `Tests-ODBC` containing unit tests. + +## Fixed + +* `SQLInteger` class is fixed (incorrectly defined as 64 bits instead of 32 bits). +* Improvement of the documentation for the SQLite data source. +* Incorrect `TimeStamp` class use in `ODBColumn >> dateTimeData` (replaced by `DateTime` class). + +## Removed + +* Unnecessary and broken `ODBCResultTable class`. # 1.13 (2025-03-15) -* fix: Use authorInitials in ODBCConnection >> workstationId -* fix: add requirement for Network-kernel package -* Improvement of the documentation +## Changed + +* Improvement of the documentation. +* Requirement for `Network-kernel package`. + +## Fixed + +* Use `authorInitials` in `ODBCConnection >> workstationId`. # 1.12 (2025-01-09) -* fix: ODBCStatement >> Execute: now supports SQL request with Unicode characters, -* fix: ODBCColumn >> stringFromBuffer now supports SQL columns with Unicode Characters. +## Fixed + +* `ODBCStatement >> Execute:` now supports SQL request with Unicode characters. +* `ODBCColumn >> stringFromBuffer` now supports SQL columns with Unicode Characters. From c1f4fbe6b1a23e37639b442e196fd7ba8ea86475 Mon Sep 17 00:00:00 2001 From: Olivier Auverlot Date: Wed, 25 Mar 2026 18:35:41 +0100 Subject: [PATCH 06/12] Add link to UnixODBC drivers page --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bcbbfbe..759a567 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,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. From 91fee329836268633cf13da9786540e682b2fb77 Mon Sep 17 00:00:00 2001 From: Olivier Auverlot Date: Sat, 28 Mar 2026 23:12:38 +0100 Subject: [PATCH 07/12] DatabaseSupport logo added --- README.md | 2 ++ assets/DatabaseSupport.png | Bin 0 -> 43397 bytes 2 files changed, 2 insertions(+) create mode 100644 assets/DatabaseSupport.png diff --git a/README.md b/README.md index 759a567..79d806e 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. diff --git a/assets/DatabaseSupport.png b/assets/DatabaseSupport.png new file mode 100644 index 0000000000000000000000000000000000000000..63dc1c5401d329f2bdccda681332a27e32d7ab75 GIT binary patch literal 43397 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�E*d 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 literal 0 HcmV?d00001 From 0e7e42af042fec1a4495fa2a9170b0bec82bfadd Mon Sep 17 00:00:00 2001 From: Olivier Auverlot Date: Sun, 29 Mar 2026 22:04:12 +0200 Subject: [PATCH 08/12] An argument of a parameterized query can now contain a string --- CHANGELOG.md | 1 + ODBC.pck.st | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1ca999..c523cca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ * `SQLInteger` class is fixed (incorrectly defined as 64 bits instead of 32 bits). * Improvement of the documentation for the SQLite data source. * Incorrect `TimeStamp` class use in `ODBColumn >> dateTimeData` (replaced by `DateTime` class). +* An argument of a parameterized query can now contain a string. ## Removed diff --git a/ODBC.pck.st b/ODBC.pck.st index c07afa7..221160f 100644 --- a/ODBC.pck.st +++ b/ODBC.pck.st @@ -1,6 +1,6 @@ -'From Cuis7.6 [latest update: #7777] on 25 March 2026 at 5:21:48 pm'! +'From Cuis7.6 [latest update: #7777] on 29 March 2026 at 10:03:28 pm'! 'Description '! -!provides: 'ODBC' 1 17! +!provides: 'ODBC' 1 18! !requires: 'Network-Kernel' 1 12 nil! !requires: 'FFI' 1 40 nil! SystemOrganization addCategory: #'ODBC-Constants'! @@ -2488,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]. From b1158004011e6ba99dbd7270f3602c019a8917e7 Mon Sep 17 00:00:00 2001 From: Olivier Auverlot Date: Sun, 29 Mar 2026 22:06:23 +0200 Subject: [PATCH 09/12] Optional `Tests-ODBC` package that provides unit tests --- CHANGELOG.md | 2 +- Tests-ODBC.pck.st | 119 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 Tests-ODBC.pck.st diff --git a/CHANGELOG.md b/CHANGELOG.md index c523cca..13f6516 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ * `ODBCConnection class >> dsn:` for a data source that does not require authentication. * Documentation about unit tests. -* Optional package `Tests-ODBC` containing unit tests. +* Optional `Tests-ODBC` package that provides unit tests. ## Fixed diff --git a/Tests-ODBC.pck.st b/Tests-ODBC.pck.st new file mode 100644 index 0000000..e823425 --- /dev/null +++ b/Tests-ODBC.pck.st @@ -0,0 +1,119 @@ +'From Cuis7.6 [latest update: #7777] on 29 March 2026 at 10:05:53 pm'! +'Description '! +!provides: 'Tests-ODBC' 1 2! +!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: 'tests' stamp: 'OA 29/Mar/2026 12:07:13'! +testSelect + | stmt rs | + + stmt := connection query: 'SELECT * FROM users'. + 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: #email) = 'john.doe@cuis.org'. + 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: 'tests' 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: 'tests' 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: 'tests' 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: 'tests' 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: 'tests' 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: '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 ! ! From 96b65145d45e8f4fb5fc8d745e47534f142c38e3 Mon Sep 17 00:00:00 2001 From: Olivier Auverlot Date: Sun, 29 Mar 2026 22:24:35 +0200 Subject: [PATCH 10/12] DML categories added --- Tests-ODBC.pck.st | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Tests-ODBC.pck.st b/Tests-ODBC.pck.st index e823425..d162e81 100644 --- a/Tests-ODBC.pck.st +++ b/Tests-ODBC.pck.st @@ -1,22 +1,22 @@ -'From Cuis7.6 [latest update: #7777] on 29 March 2026 at 10:05:53 pm'! +'From Cuis7.6 [latest update: #7777] on 29 March 2026 at 10:22:54 pm'! 'Description '! -!provides: 'Tests-ODBC' 1 2! +!provides: 'Tests-ODBC' 1 3! !requires: 'ODBC' 1 15 nil! SystemOrganization addCategory: #'Tests-ODBC'! -!classDefinition: #OdbcTests category: #'Tests-ODBC'! -TestCase subclass: #OdbcTests +!classDefinition: #ODBCTests category: #'Tests-ODBC'! +TestCase subclass: #ODBCTests instanceVariableNames: 'connection' classVariableNames: '' poolDictionaries: '' category: 'Tests-ODBC'! -!classDefinition: 'OdbcTests class' category: #'Tests-ODBC'! -OdbcTests class +!classDefinition: 'ODBCTests class' category: #'Tests-ODBC'! +ODBCTests class instanceVariableNames: ''! -!OdbcTests methodsFor: 'setUp/tearDown' stamp: 'OA 29/Mar/2026 21:58:34'! +!ODBCTests methodsFor: 'setUp/tearDown' stamp: 'OA 29/Mar/2026 21:58:34'! setUp | stmt queries | @@ -34,7 +34,7 @@ setUp stmt execute. ]! ! -!OdbcTests methodsFor: 'setUp/tearDown' stamp: 'OA 22/Mar/2026 17:03:31'! +!ODBCTests methodsFor: 'setUp/tearDown' stamp: 'OA 22/Mar/2026 17:03:31'! tearDown | stmt | @@ -43,7 +43,7 @@ tearDown connection close.! ! -!OdbcTests methodsFor: 'tests' stamp: 'OA 29/Mar/2026 12:07:13'! +!ODBCTests methodsFor: 'select' stamp: 'OA 29/Mar/2026 12:07:13'! testSelect | stmt rs | @@ -61,7 +61,7 @@ testSelect self assert: (rs at: #modified) = nil. ! ! -!OdbcTests methodsFor: 'tests' stamp: 'OA 29/Mar/2026 12:00:50'! +!ODBCTests methodsFor: 'select' stamp: 'OA 29/Mar/2026 12:00:50'! testSelectCount | stmt rs | @@ -70,7 +70,7 @@ testSelectCount self assert: (rs next at: #nbr) = 2.! ! -!OdbcTests methodsFor: 'tests' stamp: 'OA 29/Mar/2026 12:08:16'! +!ODBCTests methodsFor: 'select' stamp: 'OA 29/Mar/2026 12:08:16'! testSelectWhere | stmt rs | @@ -80,7 +80,7 @@ testSelectWhere self assert: (rs at: #login) = 'psmith'. ! ! -!OdbcTests methodsFor: 'tests' stamp: 'OA 29/Mar/2026 12:08:57'! +!ODBCTests methodsFor: 'select' stamp: 'OA 29/Mar/2026 12:08:57'! testSelectWhereOrder | stmt rs | @@ -90,7 +90,7 @@ testSelectWhereOrder self assert: (rs at: #id) = 2. ! ! -!OdbcTests methodsFor: 'tests' stamp: 'OA 29/Mar/2026 12:15:44'! +!ODBCTests methodsFor: 'select' stamp: 'OA 29/Mar/2026 12:15:44'! testSelectWherePreparedStatement | stmt rs | @@ -100,7 +100,7 @@ testSelectWherePreparedStatement self assert: (rs at: #login) = 'jdoe'. ! ! -!OdbcTests methodsFor: 'tests' stamp: 'OA 29/Mar/2026 22:00:38'! +!ODBCTests methodsFor: 'select' stamp: 'OA 29/Mar/2026 22:00:38'! testSelectWherePreparedStatementWithParameters | stmt rs | @@ -110,10 +110,10 @@ testSelectWherePreparedStatementWithParameters self assert: (rs at: #email) = 'john.doe@cuis.org'. ! ! -!OdbcTests methodsFor: 'accessing' stamp: 'OA 29/Mar/2026 11:46:25'! +!ODBCTests methodsFor: 'accessing' stamp: 'OA 29/Mar/2026 11:46:25'! connection ^connection! ! -!OdbcTests methodsFor: 'accessing' stamp: 'OA 29/Mar/2026 11:46:55'! +!ODBCTests methodsFor: 'accessing' stamp: 'OA 29/Mar/2026 11:46:55'! connection: anODBCConnection connection := anODBCConnection ! ! From 8f33e821a12d05c18eb932fd5504f480b8a9be57 Mon Sep 17 00:00:00 2001 From: Olivier Auverlot Date: Sun, 29 Mar 2026 22:36:06 +0200 Subject: [PATCH 11/12] .gitattributes file added --- .gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitattributes 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 From 685b826c4bffde17eea0aaeb099f53631d2c260a Mon Sep 17 00:00:00 2001 From: Olivier Auverlot Date: Sat, 11 Apr 2026 19:04:49 +0200 Subject: [PATCH 12/12] Corrections and tests --- .gitignore | 3 ++ CHANGELOG.md | 19 ++++--- ODBC.pck.st | 10 ++-- README.md | 41 ++++++++++++--- Tests-ODBC.pck.st | 130 ++++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 177 insertions(+), 26 deletions(-) create mode 100644 .gitignore 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 13f6516..350b4ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,25 +1,28 @@ # CHANGELOG.md -# Unreleased +# 1.19 (2026-04-11) ## Added * `ODBCConnection class >> dsn:` for a data source that does not require authentication. +* Optional `Tests-ODBC` package for unit tests. * Documentation about unit tests. -* Optional `Tests-ODBC` package that provides unit tests. +* The project's language is set to `Smalltalk` on GitHub. +* A logo for the project :) ## Fixed -* `SQLInteger` class is fixed (incorrectly defined as 64 bits instead of 32 bits). -* Improvement of the documentation for the SQLite data source. -* Incorrect `TimeStamp` class use in `ODBColumn >> dateTimeData` (replaced by `DateTime` class). +* `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`. +* Unnecessary and broken `ODBCResultTable` class. -# 1.13 (2025-03-15) +# 1.13 (2026-03-15) ## Changed @@ -30,7 +33,7 @@ * Use `authorInitials` in `ODBCConnection >> workstationId`. -# 1.12 (2025-01-09) +# 1.12 (2026-01-09) ## Fixed diff --git a/ODBC.pck.st b/ODBC.pck.st index 221160f..2a76ec4 100644 --- a/ODBC.pck.st +++ b/ODBC.pck.st @@ -1,6 +1,6 @@ -'From Cuis7.6 [latest update: #7777] on 29 March 2026 at 10:03:28 pm'! +'From Cuis7.6 [latest update: #7777] on 9 April 2026 at 9:22:04 pm'! 'Description '! -!provides: 'ODBC' 1 18! +!provides: 'ODBC' 1 19! !requires: 'Network-Kernel' 1 12 nil! !requires: 'FFI' 1 40 nil! SystemOrganization addCategory: #'ODBC-Constants'! @@ -2565,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 | @@ -2575,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 79d806e..db821ef 100644 --- a/README.md +++ b/README.md @@ -210,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'. @@ -224,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 @@ -239,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 @@ -252,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 @@ -266,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 @@ -280,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. @@ -296,4 +322,3 @@ conn commitTransaction. conn close. ``` - diff --git a/Tests-ODBC.pck.st b/Tests-ODBC.pck.st index d162e81..77473da 100644 --- a/Tests-ODBC.pck.st +++ b/Tests-ODBC.pck.st @@ -1,6 +1,6 @@ -'From Cuis7.6 [latest update: #7777] on 29 March 2026 at 10:22:54 pm'! +'From Cuis7.6 [latest update: #7777] on 11 April 2026 at 6:40:00 pm'! 'Description '! -!provides: 'Tests-ODBC' 1 3! +!provides: 'Tests-ODBC' 1 5! !requires: 'ODBC' 1 15 nil! SystemOrganization addCategory: #'Tests-ODBC'! @@ -43,17 +43,17 @@ tearDown connection close.! ! -!ODBCTests methodsFor: 'select' stamp: 'OA 29/Mar/2026 12:07:13'! +!ODBCTests methodsFor: 'select' stamp: 'OA 9/Apr/2026 19:02:05'! testSelect | stmt rs | - stmt := connection query: 'SELECT * FROM users'. + 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: #email) = 'john.doe@cuis.org'. + 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. @@ -110,6 +110,126 @@ testSelectWherePreparedStatementWithParameters 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! !